knock 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2011-2026 Carter Cole
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # knock
2
+
3
+ > Knock, knock. Who's there?
4
+
5
+ Subdomain enumeration for Node.js. Pulls candidate names from **Certificate
6
+ Transparency logs** (passive, via [crt.sh](https://crt.sh)) and a bundled
7
+ **31k-entry wordlist** (active DNS brute force), detects wildcard DNS so you
8
+ don't drown in false positives, and verifies everything with real lookups.
9
+
10
+ Zero runtime dependencies. Great for attack-surface audits, pen-test recon,
11
+ or checking what a new client actually has exposed.
12
+
13
+ [![npm version](https://img.shields.io/npm/v/knock.svg)](https://www.npmjs.com/package/knock)
14
+ [![CI](https://github.com/neopunisher/node-knock/actions/workflows/ci.yml/badge.svg)](https://github.com/neopunisher/node-knock/actions/workflows/ci.yml)
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install -g knock # CLI
20
+ npm install knock # library
21
+ ```
22
+
23
+ Requires Node.js >= 22.
24
+
25
+ ## CLI
26
+
27
+ ```bash
28
+ knock example.com
29
+ ```
30
+
31
+ ```
32
+ www.example.com 93.184.216.34 [ct+list]
33
+ api.example.com 203.0.113.10 [ct]
34
+ mail.example.com 198.51.100.7 [list]
35
+ # example.com: 3 found from 31307 candidates, 18 ct names (2 unresolved) in 41.3s
36
+ ```
37
+
38
+ Useful flags:
39
+
40
+ | Flag | Effect |
41
+ | --- | --- |
42
+ | `-p, --passive` | Certificate Transparency only — no brute force |
43
+ | `--no-ct` | wordlist brute force only |
44
+ | `--no-verify` | report CT names without resolving them |
45
+ | `--full-host` | scan the exact host given (skip the registrable-domain reduction) |
46
+ | `-l, --list <path\|subs\|org>` | custom wordlist, or a bundled one by name |
47
+ | `-c, --concurrency <n>` | parallel DNS queries (default 64) |
48
+ | `-s, --server <ip>` | DNS server to query (repeatable) |
49
+ | `-6, --ipv6` | also resolve AAAA records |
50
+ | `-w, --web` | probe http/https on each found host |
51
+ | `-j, --json` | full report as JSON |
52
+ | `-q, --quiet` | hostnames only (pipe-friendly) |
53
+
54
+ Found hosts stream to stdout as they resolve; diagnostics go to stderr, so
55
+ `knock -q example.com | sort` does what you'd hope.
56
+
57
+ ## Library
58
+
59
+ ```js
60
+ import { knock } from 'knock';
61
+
62
+ const report = await knock('example.com', { concurrency: 128 });
63
+
64
+ for (const { name, addresses, sources } of report.results) {
65
+ console.log(name, addresses, sources); // 'www.example.com', ['93.184.216.34'], ['ct', 'wordlist']
66
+ }
67
+ console.log(report.wildcard); // { detected: false, addresses: [] }
68
+ console.log(report.ct); // { enabled: true, names: 18, unresolved: [...], error: null }
69
+ console.log(report.stats); // { words, candidates, queried, found, errors, durationMs }
70
+ ```
71
+
72
+ Stream results as they're discovered, or go passive-only:
73
+
74
+ ```js
75
+ await knock('example.com', { onResult: (r) => console.log(r.name) });
76
+
77
+ // Just the Certificate Transparency names, one HTTPS request, no DNS:
78
+ import { certSubdomains } from 'knock';
79
+ const names = await certSubdomains('example.com');
80
+ ```
81
+
82
+ ### Options
83
+
84
+ | Option | Default | Description |
85
+ | --- | --- | --- |
86
+ | `words` | — | array of labels to try (`[]` for passive-only) |
87
+ | `wordlist` | bundled `subs` | path to a wordlist file |
88
+ | `ct` | `true` | pull candidates from Certificate Transparency logs |
89
+ | `ctTimeout` | `15000` | crt.sh request timeout (ms) |
90
+ | `verify` | `true` | resolve CT names over DNS |
91
+ | `concurrency` | `64` | parallel DNS queries |
92
+ | `timeout` / `tries` | `5000` / `2` | per-query DNS timeout (ms) and retries |
93
+ | `servers` | system | DNS servers to query |
94
+ | `family` | `4` | `4`, `6`, or `'any'` for A + AAAA |
95
+ | `wildcardTests` | `3` | random probes for wildcard detection (`0` disables) |
96
+ | `web` / `webTimeout` | `false` / `5000` | probe http/https on found hosts |
97
+ | `baseDomainOnly` | `true` | reduce the target to its registrable domain first |
98
+ | `icannOnly` | `false` | ignore the PSL's PRIVATE section (github.io, …) |
99
+ | `signal` | — | `AbortSignal` to cancel the run |
100
+ | `onResult` | — | callback fired per discovery |
101
+ | `resolver` / `fetch` / `psl` | built-in | injectable for testing |
102
+
103
+ The bundled wordlists are exposed as `wordlists.subs` (~31k labels,
104
+ popularity-ordered) and `wordlists.org`, and helpers `loadWordlist(path)` /
105
+ `parseWordlist(text)` / `normalizeDomain(input)` are exported too. Full
106
+ TypeScript types ship with the package.
107
+
108
+ ## How it works
109
+
110
+ 0. **Public Suffix List** — input like `https://deep.www.example.co.uk/x` is
111
+ first reduced to its registrable domain (`example.co.uk`) using a vendored
112
+ [PSL](https://publicsuffix.org) snapshot (refresh with `npm run
113
+ update-psl`), so private suffixes like `github.io` are handled correctly.
114
+ `--full-host` / `baseDomainOnly: false` scans the host exactly as given,
115
+ and the PSL helpers (`getRegistrableDomain`, `getPublicSuffix`,
116
+ `loadPublicSuffixList`) are exported for standalone use.
117
+ 1. **Certificate Transparency** — every publicly trusted TLS certificate is
118
+ logged; querying the logs for `%.example.com` reveals names that were ever
119
+ certified, including ones DNS brute forcing would never guess. Names that
120
+ no longer resolve are reported in `ct.unresolved` — often the most
121
+ interesting ones.
122
+ 2. **Wildcard detection** — a few random labels are resolved first; if the
123
+ zone answers for anything, wordlist hits that merely echo the wildcard
124
+ answer are suppressed (CT-sourced names are kept: the certificate is
125
+ evidence on its own).
126
+ 3. **Brute force** — wordlist labels are resolved through a bounded worker
127
+ pool using `node:dns` directly against your system resolvers (or servers
128
+ you pick with `-s`).
129
+
130
+ ## Development
131
+
132
+ TypeScript sources live in `src/` and run directly on Node's native type
133
+ stripping — `npm test` executes the `.ts` tests with `node --test`, no build
134
+ step needed. `npm run build` compiles `dist/` (what actually ships), and
135
+ `npm run knock -- example.com` runs the CLI from source.
136
+
137
+ ## Responsible use
138
+
139
+ DNS brute forcing generates thousands of queries and CT lookups are logged.
140
+ Only scan domains you own or are explicitly authorized to assess.
141
+
142
+ ## Publishing
143
+
144
+ Releases are published to npm from GitHub Actions via
145
+ [trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC) — no
146
+ long-lived npm tokens — with provenance attestations generated automatically.
147
+
148
+ ## License
149
+
150
+ MIT © [Carter Cole](https://github.com/neopunisher)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { readFileSync } from 'node:fs';
4
+ import process from 'node:process';
5
+ import { knock, wordlists } from './index.js';
6
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
7
+ const HELP = `knock ${version} — subdomain enumeration
8
+
9
+ Usage
10
+ knock [options] <domain> [domain ...]
11
+
12
+ Sources
13
+ Certificate Transparency logs (crt.sh, passive) and a wordlist DNS brute
14
+ force run by default; wildcard DNS answers are detected and filtered.
15
+
16
+ Options
17
+ -l, --list <path|subs|org> wordlist file, or a bundled list by name
18
+ (default: "subs", ~31k entries)
19
+ -p, --passive skip the wordlist brute force (CT lookup only)
20
+ --no-ct skip the Certificate Transparency lookup
21
+ --no-verify report CT names without resolving them
22
+ --full-host scan the exact host given, without reducing it
23
+ to the registrable domain (Public Suffix List)
24
+ -c, --concurrency <n> parallel DNS queries (default: 64)
25
+ -t, --timeout <ms> per-query DNS timeout (default: 5000)
26
+ -s, --server <ip> DNS server to use, repeatable
27
+ -6, --ipv6 also resolve AAAA records
28
+ -w, --web probe http/https on found hosts
29
+ -j, --json print the full report as JSON
30
+ -q, --quiet hostnames only, no summary
31
+ -h, --help show this help
32
+ -V, --version print the version
33
+
34
+ Examples
35
+ knock example.com
36
+ knock --passive --json example.com
37
+ knock -l org -s 1.1.1.1 -c 128 example.com
38
+
39
+ Only scan domains you own or are authorized to assess.`;
40
+ function fail(message) {
41
+ process.stderr.write(`knock: ${message}\n`);
42
+ process.exit(1);
43
+ }
44
+ function parseCliArgs() {
45
+ try {
46
+ return parseArgs({
47
+ allowPositionals: true,
48
+ options: {
49
+ list: { type: 'string', short: 'l' },
50
+ passive: { type: 'boolean', short: 'p', default: false },
51
+ 'no-ct': { type: 'boolean', default: false },
52
+ 'no-verify': { type: 'boolean', default: false },
53
+ 'full-host': { type: 'boolean', default: false },
54
+ concurrency: { type: 'string', short: 'c', default: '64' },
55
+ timeout: { type: 'string', short: 't', default: '5000' },
56
+ server: { type: 'string', short: 's', multiple: true },
57
+ ipv6: { type: 'boolean', short: '6', default: false },
58
+ web: { type: 'boolean', short: 'w', default: false },
59
+ json: { type: 'boolean', short: 'j', default: false },
60
+ quiet: { type: 'boolean', short: 'q', default: false },
61
+ help: { type: 'boolean', short: 'h', default: false },
62
+ version: { type: 'boolean', short: 'V', default: false },
63
+ },
64
+ });
65
+ }
66
+ catch (error) {
67
+ return fail(error instanceof Error ? error.message : String(error));
68
+ }
69
+ }
70
+ const { values: flags, positionals: domains } = parseCliArgs();
71
+ if (flags.help) {
72
+ console.log(HELP);
73
+ process.exit(0);
74
+ }
75
+ if (flags.version) {
76
+ console.log(version);
77
+ process.exit(0);
78
+ }
79
+ if (domains.length === 0) {
80
+ fail('no domain given (try: knock example.com, or --help)');
81
+ }
82
+ const integer = (name, value) => {
83
+ const n = Number(value);
84
+ if (!Number.isInteger(n) || n <= 0)
85
+ fail(`--${name} expects a positive integer, got "${value}"`);
86
+ return n;
87
+ };
88
+ const options = {
89
+ concurrency: integer('concurrency', flags.concurrency),
90
+ timeout: integer('timeout', flags.timeout),
91
+ servers: flags.server,
92
+ family: flags.ipv6 ? 'any' : 4,
93
+ ct: !flags['no-ct'],
94
+ verify: !flags['no-verify'],
95
+ baseDomainOnly: !flags['full-host'],
96
+ web: flags.web,
97
+ };
98
+ if (flags.passive)
99
+ options.words = [];
100
+ if (flags.list === 'subs' || flags.list === 'org')
101
+ options.wordlist = wordlists[flags.list];
102
+ else if (flags.list)
103
+ options.wordlist = flags.list;
104
+ if (flags.passive && !options.ct)
105
+ fail('--passive together with --no-ct leaves nothing to do');
106
+ const sourceTag = (sources) => sources.length > 1 ? 'ct+list' : sources[0] === 'ct' ? 'ct' : 'list';
107
+ const printResult = (result) => {
108
+ if (flags.quiet) {
109
+ console.log(result.name);
110
+ return;
111
+ }
112
+ const addresses = result.addresses.length > 0 ? result.addresses.join(' ') : '-';
113
+ const web = result.web
114
+ ? ` https:${result.web.https ?? '-'} http:${result.web.http ?? '-'}`
115
+ : '';
116
+ console.log(`${result.name} ${addresses} [${sourceTag(result.sources)}]${web}`);
117
+ };
118
+ const reports = [];
119
+ let failed = false;
120
+ for (const domain of domains) {
121
+ try {
122
+ const report = await knock(domain, {
123
+ ...options,
124
+ onResult: flags.json ? undefined : printResult,
125
+ });
126
+ reports.push(report);
127
+ if (!flags.json && !flags.quiet) {
128
+ const { stats, wildcard, ct } = report;
129
+ if (wildcard.detected) {
130
+ process.stderr.write(`! wildcard DNS on ${report.domain} (${wildcard.addresses.join(', ')}) — matching wordlist hits suppressed\n`);
131
+ }
132
+ if (ct.error)
133
+ process.stderr.write(`! ct lookup failed: ${ct.error}\n`);
134
+ const ctNote = ct.enabled ? `, ${ct.names} ct names (${ct.unresolved.length} unresolved)` : '';
135
+ process.stderr.write(`# ${report.domain}: ${stats.found} found from ${stats.candidates} candidates${ctNote} in ${(stats.durationMs / 1000).toFixed(1)}s\n`);
136
+ }
137
+ }
138
+ catch (error) {
139
+ failed = true;
140
+ process.stderr.write(`knock: ${domain}: ${error instanceof Error ? error.message : String(error)}\n`);
141
+ }
142
+ }
143
+ if (flags.json && reports.length > 0) {
144
+ console.log(JSON.stringify(reports.length === 1 ? reports[0] : reports, null, 2));
145
+ }
146
+ process.exitCode = failed ? 1 : 0;
@@ -0,0 +1,148 @@
1
+ import type { PublicSuffixRules } from './psl.ts';
2
+ export type KnockSource = 'wordlist' | 'ct';
3
+ export interface KnockWebStatuses {
4
+ https: number | null;
5
+ http: number | null;
6
+ }
7
+ export interface KnockResult {
8
+ /** Fully qualified hostname, e.g. "www.example.com". */
9
+ name: string;
10
+ /** Addresses the name resolved to (empty for unverified CT names). */
11
+ addresses: string[];
12
+ /** Where the candidate came from. */
13
+ sources: KnockSource[];
14
+ /** HTTP status codes per protocol; only present when `web: true`. */
15
+ web?: KnockWebStatuses;
16
+ }
17
+ export interface KnockWildcard {
18
+ detected: boolean;
19
+ /** Addresses returned for random non-existent labels. */
20
+ addresses: string[];
21
+ }
22
+ export interface KnockCtReport {
23
+ enabled: boolean;
24
+ /** Unique in-scope names found in Certificate Transparency logs. */
25
+ names: number;
26
+ /** CT names that no longer resolve (stale or internal-only certificates). */
27
+ unresolved: string[];
28
+ /** Error message when the CT lookup failed; the run continues without it. */
29
+ error: string | null;
30
+ }
31
+ export interface KnockStats {
32
+ words: number;
33
+ candidates: number;
34
+ queried: number;
35
+ found: number;
36
+ errors: number;
37
+ durationMs: number;
38
+ }
39
+ export interface KnockReport {
40
+ domain: string;
41
+ wildcard: KnockWildcard;
42
+ ct: KnockCtReport;
43
+ results: KnockResult[];
44
+ stats: KnockStats;
45
+ }
46
+ /** Minimal resolver surface; satisfied by dns.promises.Resolver or a test double. */
47
+ export interface KnockResolver {
48
+ resolve4(name: string): Promise<string[]>;
49
+ resolve6(name: string): Promise<string[]>;
50
+ }
51
+ /** Minimal fetch surface; satisfied by globalThis.fetch or a test double. */
52
+ export type KnockFetch = (url: string, init: {
53
+ signal: AbortSignal;
54
+ headers: Record<string, string>;
55
+ }) => Promise<{
56
+ ok: boolean;
57
+ status: number;
58
+ json(): Promise<unknown>;
59
+ }>;
60
+ export interface CertSubdomainsOptions {
61
+ /** Request timeout in milliseconds. Default 15000. */
62
+ timeout?: number;
63
+ signal?: AbortSignal;
64
+ /** Injectable fetch implementation, mainly for testing. */
65
+ fetch?: KnockFetch;
66
+ }
67
+ export interface KnockOptions {
68
+ /** Labels to try. Takes precedence over `wordlist`. Pass [] for passive-only runs. */
69
+ words?: string[];
70
+ /** Path to a wordlist file. Defaults to the bundled `wordlists.subs`. */
71
+ wordlist?: string;
72
+ /** Parallel DNS queries. Default 64. */
73
+ concurrency?: number;
74
+ /** Per-query DNS timeout in milliseconds. Default 5000. */
75
+ timeout?: number;
76
+ /** DNS retry attempts per query. Default 2. */
77
+ tries?: number;
78
+ /** DNS servers to query instead of the system resolvers. */
79
+ servers?: string[];
80
+ /** Address family: 4 (A), 6 (AAAA) or 'any' for both. Default 4. */
81
+ family?: 4 | 6 | 'any';
82
+ /** Random probes used to detect wildcard DNS. 0 disables. Default 3. */
83
+ wildcardTests?: number;
84
+ /** Pull candidates from Certificate Transparency logs. Default true. */
85
+ ct?: boolean;
86
+ /** CT request timeout in milliseconds. Default 15000. */
87
+ ctTimeout?: number;
88
+ /** Resolve CT-discovered names over DNS. Default true. */
89
+ verify?: boolean;
90
+ /** Probe http/https on each found host. Default false. */
91
+ web?: boolean;
92
+ /** Per-request web probe timeout in milliseconds. Default 5000. */
93
+ webTimeout?: number;
94
+ /**
95
+ * Reduce the target to its registrable domain via the Public Suffix List
96
+ * before enumerating (so "dev.example.co.uk" is scanned as "example.co.uk").
97
+ * Default true; set false to enumerate under the exact host given.
98
+ */
99
+ baseDomainOnly?: boolean;
100
+ /**
101
+ * When reducing to the registrable domain, use only the ICANN section of the
102
+ * Public Suffix List, ignoring private suffixes like github.io. Default false.
103
+ */
104
+ icannOnly?: boolean;
105
+ /** Injectable Public Suffix List rules, mainly for testing. */
106
+ psl?: PublicSuffixRules;
107
+ /** Abort the run; knock() rejects with the abort reason. */
108
+ signal?: AbortSignal;
109
+ /** Injectable resolver, mainly for testing. */
110
+ resolver?: KnockResolver;
111
+ /** Injectable fetch implementation used for the CT lookup. */
112
+ fetch?: KnockFetch;
113
+ /** Called with each result as it is discovered. */
114
+ onResult?: (result: KnockResult) => void;
115
+ }
116
+ /** Absolute paths to the wordlists bundled with the package. */
117
+ export declare const wordlists: {
118
+ subs: string;
119
+ org: string;
120
+ };
121
+ /**
122
+ * Reduce user input like "HTTPS://www.Example.com/path" to a bare hostname.
123
+ * Throws TypeError when no usable hostname can be extracted.
124
+ */
125
+ export declare function normalizeDomain(input: string): string;
126
+ /** Parse wordlist text into unique, lowercased labels. Blank lines and #comments are skipped. */
127
+ export declare function parseWordlist(text: string): string[];
128
+ /** Read and parse a wordlist file. */
129
+ export declare function loadWordlist(path: string): Promise<string[]>;
130
+ /**
131
+ * Query Certificate Transparency logs (via crt.sh) for names certified under
132
+ * `domain`. Purely passive: one HTTPS request, no DNS traffic to the target.
133
+ * Wildcard entries like "*.dev.example.com" are reported as "dev.example.com".
134
+ * Resolves to a sorted array of hostnames, the apex excluded.
135
+ */
136
+ export declare function certSubdomains(domain: string, options?: CertSubdomainsOptions): Promise<string[]>;
137
+ /**
138
+ * Enumerate subdomains of `domain`.
139
+ *
140
+ * Two sources feed the candidate set: Certificate Transparency logs
141
+ * (passive, `ct` option) and a wordlist brute force over DNS. Candidates
142
+ * are resolved concurrently; hosts that only echo a wildcard DNS answer are
143
+ * suppressed unless Certificate Transparency vouches for them.
144
+ */
145
+ export declare function knock(domain: string, options?: KnockOptions): Promise<KnockReport>;
146
+ export { publicSuffixListPath, parsePublicSuffixList, loadPublicSuffixList, getPublicSuffix, getRegistrableDomain, } from './psl.ts';
147
+ export type { PublicSuffixRule, PublicSuffixRules, PublicSuffixOptions } from './psl.ts';
148
+ export default knock;