dio-lookup 0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dio-lookup.ts +148 -0
  4. package/package.json +26 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 disclose.io
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,59 @@
1
+ # dio-lookup
2
+
3
+ Pipe internet assets to [lookup.disclose.io](https://lookup.disclose.io) and get the right **security-disclosure contact** for each — as JSONL, built for recon pipelines.
4
+
5
+ Give it a domain, IP, ASN, URL, email, package, repo, container, cloud resource, mobile app, hardware, browser extension, or org name; get back the owner and where to report a vulnerability (security.txt, bug bounty program, VDP, PSIRT, national CERT).
6
+
7
+ ```bash
8
+ subfinder -d example.com | httpx -silent | dio-lookup > contacts.jsonl
9
+ cat hosts.txt | dio-lookup -c 8
10
+ dio-lookup cloudflare.com npm:express gh:facebook/react
11
+ ```
12
+
13
+ ## Install
14
+
15
+ **With [Bun](https://bun.sh) (run from source):**
16
+
17
+ ```bash
18
+ bun install -g dio-lookup # or: git clone + bun link
19
+ ```
20
+
21
+ **Prebuilt binary** (no runtime needed) — grab the right asset from [Releases](https://github.com/disclose/dio-lookup/releases), then:
22
+
23
+ ```bash
24
+ chmod +x dio-lookup && sudo mv dio-lookup /usr/local/bin/
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```
30
+ dio-lookup [options] [asset ...]
31
+ cat hosts.txt | dio-lookup [options]
32
+
33
+ -c, --concurrency N parallel requests (default 5)
34
+ -k, --key KEY API key (raises rate limits); or set DIO_API_KEY
35
+ --api URL API endpoint (default https://lookup.disclose.io/api/lookup)
36
+ --full emit the full LookupResult instead of the compact summary
37
+ -V, --version print version
38
+ -h, --help help
39
+ ```
40
+
41
+ One JSON object per asset on stdout. Compact form:
42
+
43
+ ```json
44
+ {"input":"cloudflare.com","assetType":"domain","status":"complete","organization":"Cloudflare","jurisdiction":"US","contacts":[{"type":"security_txt","value":"https://www.cloudflare.com/.well-known/security.txt","confidence":"high"}]}
45
+ ```
46
+
47
+ Pull just the reporting channels with `jq`:
48
+
49
+ ```bash
50
+ cat hosts.txt | dio-lookup | jq -r 'select(.status=="complete") | "\(.input)\t\(.contacts[0].value)"'
51
+ ```
52
+
53
+ ## Notes
54
+
55
+ - **Free and anonymous.** A free API key only raises rate limits — request one by emailing [hello@disclose.io](mailto:hello@disclose.io). Pass it with `-k` or `DIO_API_KEY`.
56
+ - Honors the API's `Retry-After` on 429 and retries transient failures.
57
+ - `--full` emits the complete `LookupResult` (attribution, contacts, resolution chain, data sources) — see the [OpenAPI spec](https://lookup.disclose.io/openapi.yaml).
58
+
59
+ A [disclose.io](https://disclose.io) project. MIT licensed.
package/dio-lookup.ts ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * dio-lookup — pipe internet assets to lookup.disclose.io, get security
4
+ * disclosure contacts back as JSONL.
5
+ *
6
+ * Built for recon pipelines:
7
+ * subfinder -d example.com | httpx -silent | dio-lookup
8
+ * cat hosts.txt | dio-lookup --concurrency 8 > contacts.jsonl
9
+ * dio-lookup cloudflare.com npm:express gh:facebook/react
10
+ *
11
+ * Each input asset (domain, IP, ASN, URL, email, package, repo, container,
12
+ * cloud resource, mobile app, hardware, extension, org name) becomes one JSON
13
+ * object on stdout. Anonymous + free; an optional API key raises rate limits.
14
+ */
15
+
16
+ const VERSION = '0.1.0';
17
+ const DEFAULT_API = 'https://lookup.disclose.io/api/lookup';
18
+
19
+ interface Options {
20
+ concurrency: number;
21
+ api: string;
22
+ apiKey?: string;
23
+ full: boolean;
24
+ inputs: string[];
25
+ }
26
+
27
+ export function parseArgs(argv: string[]): Options | { help: true } | { version: true } {
28
+ const o: Options = { concurrency: 5, api: DEFAULT_API, full: false, inputs: [] };
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const a = argv[i];
31
+ if (a === '-h' || a === '--help') return { help: true };
32
+ if (a === '-V' || a === '--version') return { version: true };
33
+ else if (a === '-c' || a === '--concurrency') o.concurrency = Math.max(1, parseInt(argv[++i] ?? '5', 10) || 5);
34
+ else if (a === '--api') o.api = argv[++i] ?? DEFAULT_API;
35
+ else if (a === '-k' || a === '--key') o.apiKey = argv[++i];
36
+ else if (a === '--full') o.full = true;
37
+ else if (a.startsWith('-')) { process.stderr.write(`unknown flag: ${a}\n`); return { help: true }; }
38
+ else o.inputs.push(a);
39
+ }
40
+ return o;
41
+ }
42
+
43
+ const HELP = `dio-lookup ${VERSION} — security-disclosure contacts for any internet asset
44
+
45
+ USAGE
46
+ dio-lookup [options] [asset ...]
47
+ cat hosts.txt | dio-lookup [options]
48
+
49
+ OPTIONS
50
+ -c, --concurrency N parallel requests (default 5)
51
+ -k, --key KEY API key (raises rate limits); or set DIO_API_KEY
52
+ --api URL API endpoint (default ${DEFAULT_API})
53
+ --full emit the full LookupResult instead of the compact summary
54
+ -V, --version print version
55
+ -h, --help this help
56
+
57
+ OUTPUT
58
+ One JSON object per asset on stdout (JSONL). Compact form:
59
+ {"input","assetType","status","organization","jurisdiction","contacts":[{type,value,confidence}]}
60
+
61
+ EXAMPLES
62
+ dio-lookup cloudflare.com
63
+ subfinder -d example.com | httpx -silent | dio-lookup -c 8 > contacts.jsonl
64
+ echo npm:express | dio-lookup --full | jq .
65
+
66
+ A disclose.io project — https://lookup.disclose.io`;
67
+
68
+ async function readStdinLines(): Promise<string[]> {
69
+ if (process.stdin.isTTY) return [];
70
+ const text = await new Response(Bun.stdin.stream()).text();
71
+ return text.split('\n').map(l => l.trim()).filter(Boolean);
72
+ }
73
+
74
+ interface Contact { type: string; value: string; confidence: string }
75
+ interface LookupResult {
76
+ input: string; assetType?: string; status?: string;
77
+ attribution?: { organization?: string; jurisdiction?: string };
78
+ contacts?: Contact[];
79
+ }
80
+
81
+ export async function lookupOne(input: string, o: Options): Promise<Record<string, unknown>> {
82
+ const headers: Record<string, string> = { 'Content-Type': 'application/json', 'User-Agent': `dio-lookup/${VERSION}` };
83
+ const key = o.apiKey ?? process.env.DIO_API_KEY;
84
+ if (key) headers['Authorization'] = `Bearer ${key}`;
85
+
86
+ // Up to 3 attempts, honoring Retry-After on 429.
87
+ for (let attempt = 0; attempt < 3; attempt++) {
88
+ try {
89
+ const res = await fetch(o.api, { method: 'POST', headers, body: JSON.stringify({ input }) });
90
+ if (res.status === 429) {
91
+ const wait = Math.min(30, parseInt(res.headers.get('retry-after') ?? '2', 10) || 2);
92
+ await Bun.sleep(wait * 1000);
93
+ continue;
94
+ }
95
+ const body = await res.json() as LookupResult;
96
+ if (o.full) return body as unknown as Record<string, unknown>;
97
+ return {
98
+ input,
99
+ assetType: body.assetType ?? null,
100
+ status: body.status ?? null,
101
+ organization: body.attribution?.organization ?? null,
102
+ jurisdiction: body.attribution?.jurisdiction ?? null,
103
+ contacts: (body.contacts ?? []).map(c => ({ type: c.type, value: c.value, confidence: c.confidence })),
104
+ };
105
+ } catch (err) {
106
+ if (attempt === 2) return { input, error: String(err).slice(0, 200) };
107
+ await Bun.sleep(1000);
108
+ }
109
+ }
110
+ return { input, error: 'exhausted retries' };
111
+ }
112
+
113
+ // Bounded-concurrency worker pool over the input list, preserving nothing about
114
+ // order (recon pipelines don't need it) — emit as each completes.
115
+ export async function run(inputs: string[], o: Options): Promise<number> {
116
+ let idx = 0;
117
+ let failures = 0;
118
+ const out = (obj: Record<string, unknown>) => {
119
+ if (obj.error) failures++;
120
+ process.stdout.write(JSON.stringify(obj) + '\n');
121
+ };
122
+ const worker = async () => {
123
+ while (idx < inputs.length) {
124
+ const i = idx++;
125
+ out(await lookupOne(inputs[i], o));
126
+ }
127
+ };
128
+ await Promise.all(Array.from({ length: Math.min(o.concurrency, inputs.length) }, worker));
129
+ return failures;
130
+ }
131
+
132
+ export { VERSION, HELP };
133
+ export type { Options, LookupResult, Contact };
134
+
135
+ // Only run the CLI when executed directly (`bun dio-lookup.ts`), not when imported
136
+ // by the test suite. This keeps parseArgs/lookupOne/run unit-testable in isolation.
137
+ if (import.meta.main) {
138
+ const parsed = parseArgs(process.argv.slice(2));
139
+ if ('help' in parsed) { console.log(HELP); process.exit(0); }
140
+ if ('version' in parsed) { console.log(VERSION); process.exit(0); }
141
+
142
+ const stdinInputs = await readStdinLines();
143
+ const inputs = [...parsed.inputs, ...stdinInputs];
144
+ if (inputs.length === 0) { console.error('no input assets (pass as args or pipe via stdin); --help for usage'); process.exit(2); }
145
+
146
+ const failures = await run(inputs, parsed);
147
+ process.exit(failures > 0 && failures === inputs.length ? 1 : 0);
148
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "dio-lookup",
3
+ "version": "0.1.0",
4
+ "description": "Pipe internet assets to lookup.disclose.io and get security-disclosure contacts as JSONL.",
5
+ "type": "module",
6
+ "bin": {
7
+ "dio-lookup": "./dio-lookup.ts"
8
+ },
9
+ "files": [
10
+ "dio-lookup.ts"
11
+ ],
12
+ "scripts": {
13
+ "start": "bun dio-lookup.ts",
14
+ "build": "bun build ./dio-lookup.ts --compile --outfile dist/dio-lookup"
15
+ },
16
+ "engines": {
17
+ "bun": ">=1.0.0"
18
+ },
19
+ "license": "MIT",
20
+ "homepage": "https://lookup.disclose.io",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/disclose/dio-lookup.git"
24
+ },
25
+ "keywords": ["security", "vulnerability-disclosure", "security.txt", "recon", "bug-bounty", "disclose.io", "osint"]
26
+ }