knock 0.1.0 → 1.1.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/llms.txt ADDED
@@ -0,0 +1,70 @@
1
+ # knock
2
+
3
+ > Subdomain enumeration for Node.js (>= 22), as a library, a CLI, and an MCP server. Combines Certificate Transparency logs (passive, via crt.sh) with a wordlist DNS brute force (bundled ~31k labels), detects and filters wildcard DNS, and verifies names with real lookups. ESM-only, TypeScript types included, zero runtime dependencies. Only scan domains you own or are authorized to assess.
4
+
5
+ Install with `npm install knock` (library) or run `npx knock example.com` (CLI). Targets are reduced to their registrable domain via a bundled Public Suffix List before scanning ("https://a.www.example.co.uk/x" -> "example.co.uk") unless told otherwise.
6
+
7
+ ## Library
8
+
9
+ ```js
10
+ import { knock, certSubdomains } from 'knock';
11
+
12
+ // Full run: CT + wordlist brute force. Takes ~30-90s with the default wordlist.
13
+ const report = await knock('example.com', {
14
+ concurrency: 64, // parallel DNS queries
15
+ onResult: (r) => console.log(r.name), // streamed as found
16
+ });
17
+
18
+ // Passive only: CT names, verified over DNS, no brute force.
19
+ await knock('example.com', { words: [] });
20
+
21
+ // Just the CT names, unverified: one HTTPS request, no DNS.
22
+ const names = await certSubdomains('example.com'); // string[], sorted, apex excluded
23
+ ```
24
+
25
+ `knock(domain, options?)` resolves to a `KnockReport`:
26
+
27
+ ```ts
28
+ {
29
+ domain: string; // the domain actually scanned
30
+ results: { name: string; addresses: string[]; sources: ('wordlist' | 'ct')[];
31
+ web?: { https: number | null; http: number | null } }[];
32
+ wildcard: { detected: boolean; addresses: string[] };
33
+ ct: { enabled: boolean; names: number; unresolved: string[]; error: string | null };
34
+ stats: { words: number; candidates: number; queried: number; found: number;
35
+ errors: number; durationMs: number };
36
+ }
37
+ ```
38
+
39
+ Options (all optional): `words` (string[], overrides `wordlist`; `[]` = passive), `wordlist` (file path; default `wordlists.subs`), `ct` (true), `ctTimeout` (15000), `verify` (true; resolve CT names), `concurrency` (64), `timeout` (5000 ms per DNS query), `tries` (2), `servers` (DNS server IPs), `family` (4 | 6 | 'any'; default 4), `wildcardTests` (3; 0 disables), `web` (false; HEAD http/https on found hosts), `webTimeout` (5000), `baseDomainOnly` (true), `icannOnly` (false; ignore private PSL suffixes like github.io), `signal` (AbortSignal), `onResult` (callback), and test injectables `resolver`, `fetch`, `psl`.
40
+
41
+ Behavior worth knowing:
42
+ - A CT lookup failure does not reject; it is reported in `report.ct.error` and the run continues with the wordlist.
43
+ - Wordlist hits that only return the wildcard DNS answer are dropped; CT-sourced names are kept even under a wildcard.
44
+ - `report.ct.unresolved` lists CT names that no longer resolve (stale or internal hosts).
45
+ - `knock()` rejects with the abort reason when `signal` aborts, and with a `TypeError` for unusable input.
46
+
47
+ Other exports: `normalizeDomain(input)`, `parseWordlist(text)`, `loadWordlist(path)`, `wordlists` (`{ subs, org }` absolute paths), `getRegistrableDomain(host, rules, { icannOnly })`, `getPublicSuffix(host, rules, opts)`, `loadPublicSuffixList(path?)`, `parsePublicSuffixList(text)`, `publicSuffixListPath`, and all the types (`KnockOptions`, `KnockReport`, `KnockResult`, ...). Default export is `knock`.
48
+
49
+ ## CLI
50
+
51
+ ```
52
+ knock [options] <domain> [domain ...]
53
+ ```
54
+
55
+ `-p/--passive` (CT only), `--no-ct`, `--no-verify`, `--full-host` (skip registrable-domain reduction), `-l/--list <path|subs|org>`, `-c/--concurrency <n>`, `-t/--timeout <ms>`, `-s/--server <ip>` (repeatable), `-6/--ipv6`, `-w/--web`, `-j/--json` (full report), `-q/--quiet` (hostnames only), `--mcp`. Results go to stdout, diagnostics to stderr; exit code 1 if any domain failed.
56
+
57
+ ## MCP server
58
+
59
+ `knock --mcp` serves the Model Context Protocol over stdio. Register it with, for example, `claude mcp add knock -- npx -y knock --mcp`, or in a JSON client config: `{"command": "npx", "args": ["-y", "knock", "--mcp"]}`.
60
+
61
+ Tools (all read-only; results include `structuredContent` plus a JSON text block):
62
+ - `knock_enumerate` — full `knock()` run. Args: `domain` (required), `passive`, `ct`, `verify`, `fullHost`, `wordlist` ("subs" | "org"), `words`, `concurrency`, `timeout`, `servers`, `ipv6`, `web`. Returns the `KnockReport`. Sends progress notifications per found host when the call carries a progress token, and supports cancellation.
63
+ - `knock_ct_lookup` — `certSubdomains()`. Args: `domain`, `timeout`. Returns `{ domain, names }`.
64
+ - `knock_registrable_domain` — offline PSL lookup. Args: `host`, `icannOnly`. Returns `{ host, publicSuffix, registrableDomain }`.
65
+
66
+ ## Links
67
+
68
+ - [README](https://github.com/neopunisher/node-knock#readme): full documentation
69
+ - [Changelog](https://github.com/neopunisher/node-knock/blob/main/CHANGELOG.md)
70
+ - [npm](https://www.npmjs.com/package/knock)
package/package.json CHANGED
@@ -1,29 +1,63 @@
1
1
  {
2
- "name": "knock", "description": "Attempts to enumerate subdomains of a domain",
3
- "version": "0.1.0",
4
- "tags": [
2
+ "name": "knock",
3
+ "version": "1.1.0",
4
+ "description": "Knock, knock. Who's there? Subdomain enumeration via Certificate Transparency and wordlist DNS brute force, with wildcard detection. CLI, library and MCP server. Zero dependencies.",
5
+ "keywords": [
5
6
  "domain",
6
7
  "subdomain",
7
8
  "dns",
9
+ "enumeration",
10
+ "recon",
11
+ "osint",
12
+ "pentest",
13
+ "security",
8
14
  "util",
9
- "utility"
15
+ "utility",
16
+ "mcp",
17
+ "mcp-server",
18
+ "model-context-protocol"
10
19
  ],
11
- "author": { "name": "Carter Cole", "email": "node@cartercole.com" },
12
- "maintainers": [{ "name": "Carter Cole", "email": "node@cartercole.com" }],
13
- "homepage": "http://blog.cartercole.com",
20
+ "author": "Carter Cole <node@cartercole.com>",
21
+ "license": "MIT",
22
+ "homepage": "https://github.com/neopunisher/node-knock#readme",
14
23
  "repository": {
15
- "type": "git", "url": "git@github.com:neopunisher/node-knock.git"
24
+ "type": "git",
25
+ "url": "git+https://github.com/neopunisher/node-knock.git"
16
26
  },
17
- "main": "lib.js",
18
- "licenses": [{
19
- "type": "MIT", "url": "http://www.opensource.org/licenses/mit-license.php"
20
- }],
21
- "bugs": { "url": "https://github.com/neopunisher/node-knock/issues" },
22
- "dependencies": {
23
- "request": "*",
24
- "tldtools": "*",
25
- "optimist": "*",
26
- "glob" : "*",
27
- "step" : "*"
27
+ "bugs": {
28
+ "url": "https://github.com/neopunisher/node-knock/issues"
29
+ },
30
+ "type": "module",
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "bin": {
41
+ "knock": "dist/cli.js"
42
+ },
43
+ "files": [
44
+ "dist/",
45
+ "lists/",
46
+ "llms.txt"
47
+ ],
48
+ "engines": {
49
+ "node": ">=22"
50
+ },
51
+ "scripts": {
52
+ "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
53
+ "typecheck": "tsc",
54
+ "test": "node --test \"test/**/*.test.ts\"",
55
+ "knock": "node src/cli.ts",
56
+ "update-psl": "node scripts/update-psl.mjs",
57
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^22.20.4",
61
+ "typescript": "^7.0.2"
28
62
  }
29
63
  }
package/.npmignore DELETED
@@ -1 +0,0 @@
1
- node_modules/*
package/knock.js DELETED
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
- var argv = require('optimist').argv,
3
- knock = require('./lib.js'),
4
- _ = require('underscore');
5
-
6
- _.defer(function(){
7
- _.each(argv._,function doKnock(domain){
8
- knock(domain);
9
- });
10
- });
package/lib.js DELETED
@@ -1,49 +0,0 @@
1
- var _ = require('underscore')._,
2
- glob = require('glob'),
3
- tldtools = require('tldtools').init(function () {
4
- //console.log('tldsready')
5
- }),
6
- dns = require('dns'),
7
- request = require('request'),
8
- Step = require('step'),
9
- fs = require('fs'),
10
- dnstest = function () {},
11
- webtest = function () {},
12
- basedomain = function (domain) {
13
- if (domain.indexOf('http') != 0) {
14
- domain = 'http://' + domain + '/'
15
- }
16
- var res = tldtools.extract(domain);
17
- return res.inspect.useful() ? res.domain + '.' + res.tld : false;
18
- }
19
- module.exports.opts = {
20
- type: dnstest,
21
- types: [dnstest, webtest],
22
- verify: true,
23
- wildcard: {
24
- tests: 1,
25
- neverthere: 'knocknpmknocknpm'
26
- }
27
- };
28
-
29
- //dns.resolve4(domain, cb);
30
-
31
- module.exports = function knock(domain, opts, cb) {
32
- if (_.isFunction(opts)) {
33
- console.log('swaped for cb', module.exports.opts);
34
- cb = opts;
35
- opts = module.export.opts
36
- } else if (_.isObject(opts)) {
37
- opts = _.extend(module.exports.opts, opts)
38
- console.log('extended', opts)
39
- }
40
-
41
- var domain = basedomain(domain);
42
- if (domain === false) {
43
- cb('invalid domain');
44
- } else {
45
- console.log('basedomain', domain);
46
-
47
-
48
- }
49
- }