Skill: arp-scan

Patterns for ARP table reading, ping-sweep discovery, and OUI vendor lookup.

7 stars

Best use case

Skill: arp-scan is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Patterns for ARP table reading, ping-sweep discovery, and OUI vendor lookup.

Teams using Skill: arp-scan should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/arp-scan/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/hardware-iot/network-device-mapper/skills/arp-scan/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/arp-scan/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How Skill: arp-scan Compares

Feature / AgentSkill: arp-scanStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Patterns for ARP table reading, ping-sweep discovery, and OUI vendor lookup.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Skill: arp-scan

Patterns for ARP table reading, ping-sweep discovery, and OUI vendor lookup.

## ARP table parsing

Read the system ARP table after a ping sweep. The `arp -a` output format differs between macOS and Linux.

```ts
// packages/server/src/scanner.ts
import { execFile } from 'child_process';
import { promisify } from 'util';

const execFileAsync = promisify(execFile);

interface ArpEntry {
  ip: string;
  mac: string;
}

async function readArpTable(): Promise<ArpEntry[]> {
  const { stdout } = await execFileAsync('arp', ['-a']);
  const entries: ArpEntry[] = [];

  // macOS / BSD: "hostname (192.168.1.1) at d8:47:32:11:22:33 on en0 ifscope [ethernet]"
  // Linux:       "hostname (192.168.1.1) at D8:47:32:11:22:33 [ether] on eth0"
  // Incomplete:  "(192.168.1.100) at <incomplete> on en0"
  const RE = /\((\d{1,3}(?:\.\d{1,3}){3})\)\s+at\s+([0-9a-fA-F:]{17})/g;
  let m: RegExpExecArray | null;
  while ((m = RE.exec(stdout)) !== null) {
    entries.push({ ip: m[1], mac: m[2].toLowerCase() });
  }
  return entries;
}
```

## Ping sweep

Send ICMP pings to all addresses in the subnet to populate the ARP table before reading it.
Use `execFile` with the system `ping` command. Errors are ignored (host offline = exit code 1).

```ts
import { CIDR } from 'cidr-tools';  // npm i cidr-tools

async function pingSweep(cidr: string, timeoutSec = 1): Promise<void> {
  const ips = CIDR.expand(cidr);  // returns string[]
  // Ping all hosts concurrently. Individual failures are expected.
  await Promise.allSettled(
    ips.map((ip) =>
      execFileAsync('ping', ['-c', '1', '-W', String(timeoutSec * 1000), ip]).catch(() => {})
    )
  );
}
```

For large subnets (e.g. /16 = 65534 hosts) batch into groups of 256 to avoid system limits:

```ts
async function pingSweepBatched(cidr: string, batchSize = 256, timeoutSec = 1) {
  const ips = CIDR.expand(cidr);
  for (let i = 0; i < ips.length; i += batchSize) {
    await pingSweep_batch(ips.slice(i, i + batchSize), timeoutSec);
  }
}
```

## Full scan function

```ts
import type { Database } from 'better-sqlite3';
import { lookupVendor } from './oui';
import { resolveHostname } from './dns';

export interface ScanProgress {
  progress: number;      // 0-100
  found: number;
  checking: string | null;
}

export type ProgressCallback = (p: ScanProgress) => void;

export async function runScan(
  db: Database,
  cidr: string,
  onProgress: ProgressCallback
): Promise<number> {
  // 1. Ping sweep to populate ARP table
  onProgress({ progress: 0, found: 0, checking: 'pinging...' });
  await pingSweep(cidr, 1);

  // 2. Read ARP table
  const arpEntries = await readArpTable();
  const now = Date.now();

  // 3. Create scan_history record
  const scanRow = db
    .prepare(`INSERT INTO scan_history (started_at, subnet) VALUES (@started_at, @subnet)`)
    .run({ started_at: now, subnet: cidr });
  const scanId = scanRow.lastInsertRowid as number;

  // 4. Upsert each discovered device
  const upsert = db.prepare(`
    INSERT INTO devices (mac, ip, hostname, vendor, is_gateway, first_seen, last_seen)
    VALUES (@mac, @ip, @hostname, @vendor, @is_gateway, @now, @now)
    ON CONFLICT(mac) DO UPDATE SET
      ip        = excluded.ip,
      hostname  = excluded.hostname,
      vendor    = COALESCE(excluded.vendor, vendor),
      last_seen = excluded.last_seen
  `);

  const total = arpEntries.length;
  for (let i = 0; i < total; i++) {
    const { ip, mac } = arpEntries[i];
    onProgress({ progress: Math.round(((i + 1) / total) * 100), found: i + 1, checking: ip });

    const hostname = await resolveHostname(ip);
    const vendor = lookupVendor(mac);
    const is_gateway = (i === 0 && ip.endsWith('.1')) ? 1 : 0;  // heuristic: first .1 = gateway

    upsert.run({ mac, ip, hostname, vendor, is_gateway, now });
  }

  // 5. Finalize scan record
  db.prepare(`
    UPDATE scan_history
    SET finished_at = @now, device_count = @count, status = 'done'
    WHERE id = @id
  `).run({ now: Date.now(), count: total, id: scanId });

  return scanId;
}
```

## Gateway detection

The gateway is the device whose IP ends in `.1` in the subnet (e.g. `192.168.1.1` for `192.168.1.0/24`).
A more reliable method parses the routing table:

```ts
async function detectGateway(cidr: string): Promise<string | null> {
  try {
    // macOS
    const { stdout } = await execFileAsync('route', ['-n', 'get', cidr.split('/')[0]]);
    const m = stdout.match(/gateway:\s+([\d.]+)/);
    return m ? m[1] : null;
  } catch {
    return null;
  }
}
```

## Hostname resolution

```ts
// packages/server/src/dns.ts
import dns from 'dns/promises';

export async function resolveHostname(ip: string): Promise<string | null> {
  try {
    const result = await dns.reverse(ip);
    return result[0] ?? null;
  } catch {
    return null;
  }
}
```

## Scan progress via SSE

Stream scan progress to the browser using Server-Sent Events instead of polling:

```ts
// packages/server/src/routes/scan.ts
router.get('/stream', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const send = (data: object) => res.write(`data: ${JSON.stringify(data)}\n\n`);

  const unsubscribe = scanEmitter.on('progress', send);
  req.on('close', unsubscribe);
});

// In scanner.ts, emit via scanEmitter
scanEmitter.emit('progress', { progress, found, checking });
```

Client side:

```ts
const es = new EventSource('/api/scan/stream');
es.onmessage = (e) => {
  const data = JSON.parse(e.data);
  useNetmapStore.getState().setScan(data);
};
// close when scan is done
```

## Known `arp -a` quirks

- macOS reports `<incomplete>` for hosts that did not respond to ARP; skip these.
- Some Linux distros require `arp-scan` package instead of base `arp`. Check availability with `which arp`.
- `arp -a` only shows hosts that have communicated recently. Run the ping sweep first to ensure entries are fresh.
- MAC addresses from `arp -a` are lowercase on Linux but mixed-case on macOS. Normalise with `.toLowerCase()`.

## OUI database

The OUI file is the IEEE MA-L registry in the format distributed at:
`https://regauth.standards.ieee.org/standards-ra-web/pub/view.html#registries`

Store it at `packages/server/data/oui.txt` and load once at startup.
Prefix format in the file is `AA-BB-CC` (hyphen separated); normalise to `AA:BB:CC` for map keys.
The lookup key is the first 3 octets of the MAC: `mac.toUpperCase().slice(0, 8)`.

## SQLite schema reminder

```sql
CREATE TABLE IF NOT EXISTS devices (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  mac         TEXT    NOT NULL UNIQUE,
  ip          TEXT    NOT NULL,
  hostname    TEXT,
  vendor      TEXT,
  label       TEXT,
  grp         TEXT,      -- NOT 'group' - reserved word in SQL
  is_gateway  INTEGER NOT NULL DEFAULT 0,
  first_seen  INTEGER NOT NULL,
  last_seen   INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS scan_history (
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  started_at   INTEGER NOT NULL,
  finished_at  INTEGER,
  subnet       TEXT    NOT NULL,
  device_count INTEGER DEFAULT 0,
  status       TEXT    NOT NULL DEFAULT 'running'  -- running | done | failed
);
```

WAL mode is required to allow concurrent reads during a long scan:

```ts
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
```

Related Skills

secret-scanner

7
from heldernoid/agentic-build-templates

Scan codebases for accidentally committed secrets using the secret-scanner CLI.

container-image-scanner

7
from heldernoid/agentic-build-templates

No description provided.

Skill: port-scanner CLI

7
from heldernoid/agentic-build-templates

## When to use

Skill: Network scanning with Node.js net.Socket

7
from heldernoid/agentic-build-templates

## When to use

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.