recker 1.0.15-next.50d74b2 → 1.0.15-next.64a2dc9
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/README.md +10 -1
- package/dist/ai/providers/anthropic.d.ts.map +1 -1
- package/dist/ai/providers/anthropic.js +4 -1
- package/dist/ai/providers/base.d.ts.map +1 -1
- package/dist/ai/providers/base.js +7 -2
- package/dist/ai/rate-limiter.d.ts.map +1 -1
- package/dist/ai/rate-limiter.js +4 -1
- package/dist/bench/generator.d.ts.map +1 -1
- package/dist/bench/generator.js +7 -3
- package/dist/bench/stats.d.ts.map +1 -1
- package/dist/bench/stats.js +43 -10
- package/dist/cache/memory-storage.d.ts.map +1 -1
- package/dist/cache/memory-storage.js +3 -2
- package/dist/cli/handler.js +14 -14
- package/dist/cli/index.js +582 -49
- package/dist/cli/presets.js +5 -5
- package/dist/cli/tui/ai-chat.js +10 -10
- package/dist/cli/tui/load-dashboard.d.ts.map +1 -1
- package/dist/cli/tui/load-dashboard.js +96 -55
- package/dist/cli/tui/scroll-buffer.d.ts +1 -1
- package/dist/cli/tui/scroll-buffer.d.ts.map +1 -1
- package/dist/cli/tui/scroll-buffer.js +2 -2
- package/dist/cli/tui/shell.d.ts +3 -0
- package/dist/cli/tui/shell.d.ts.map +1 -1
- package/dist/cli/tui/shell.js +173 -10
- package/dist/cli/tui/websocket.js +17 -17
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +18 -26
- package/dist/core/errors.d.ts +109 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +214 -1
- package/dist/core/request-promise.d.ts.map +1 -1
- package/dist/core/request-promise.js +5 -6
- package/dist/core/response.d.ts.map +1 -1
- package/dist/core/response.js +5 -6
- package/dist/dns/propagation.d.ts +3 -1
- package/dist/dns/propagation.d.ts.map +1 -1
- package/dist/dns/propagation.js +99 -59
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +10 -11
- package/dist/mcp/embeddings-loader.d.ts.map +1 -1
- package/dist/mcp/embeddings-loader.js +12 -2
- package/dist/mcp/geoip-loader.d.ts +11 -0
- package/dist/mcp/geoip-loader.d.ts.map +1 -0
- package/dist/mcp/geoip-loader.js +107 -0
- package/dist/mcp/ip-intel.d.ts +28 -0
- package/dist/mcp/ip-intel.d.ts.map +1 -0
- package/dist/mcp/ip-intel.js +209 -0
- package/dist/mcp/search/hybrid-search.d.ts.map +1 -1
- package/dist/mcp/search/hybrid-search.js +5 -1
- package/dist/mcp/search/math.d.ts.map +1 -1
- package/dist/mcp/search/math.js +5 -1
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +114 -1
- package/dist/plugins/compression.js +4 -2
- package/dist/plugins/har-player.d.ts.map +1 -1
- package/dist/plugins/har-player.js +8 -11
- package/dist/plugins/odata.d.ts.map +1 -1
- package/dist/plugins/odata.js +5 -2
- package/dist/protocols/ftp.d.ts.map +1 -1
- package/dist/protocols/ftp.js +69 -16
- package/dist/protocols/sftp.d.ts.map +1 -1
- package/dist/protocols/sftp.js +13 -3
- package/dist/protocols/telnet.d.ts.map +1 -1
- package/dist/protocols/telnet.js +25 -6
- package/dist/transport/base-udp.d.ts.map +1 -1
- package/dist/transport/base-udp.js +7 -4
- package/dist/transport/udp-response.d.ts.map +1 -1
- package/dist/transport/udp-response.js +10 -3
- package/dist/transport/udp.d.ts.map +1 -1
- package/dist/transport/udp.js +5 -1
- package/dist/transport/undici.d.ts.map +1 -1
- package/dist/transport/undici.js +75 -63
- package/dist/utils/agent-manager.d.ts +1 -0
- package/dist/utils/agent-manager.d.ts.map +1 -1
- package/dist/utils/agent-manager.js +11 -0
- package/dist/utils/client-pool.d.ts.map +1 -1
- package/dist/utils/client-pool.js +4 -1
- package/dist/utils/dns-toolkit.d.ts +88 -1
- package/dist/utils/dns-toolkit.d.ts.map +1 -1
- package/dist/utils/dns-toolkit.js +704 -6
- package/dist/utils/doh.d.ts.map +1 -1
- package/dist/utils/doh.js +13 -16
- package/dist/utils/download.d.ts.map +1 -1
- package/dist/utils/download.js +10 -11
- package/dist/utils/rdap.d.ts +9 -0
- package/dist/utils/rdap.d.ts.map +1 -1
- package/dist/utils/rdap.js +78 -9
- package/dist/utils/security-grader.d.ts +47 -0
- package/dist/utils/security-grader.d.ts.map +1 -0
- package/dist/utils/security-grader.js +637 -0
- package/dist/utils/sparkline.d.ts +18 -0
- package/dist/utils/sparkline.d.ts.map +1 -0
- package/dist/utils/sparkline.js +55 -0
- package/dist/utils/sse.d.ts.map +1 -1
- package/dist/utils/sse.js +5 -6
- package/dist/utils/system-metrics.d.ts +26 -0
- package/dist/utils/system-metrics.d.ts.map +1 -0
- package/dist/utils/system-metrics.js +81 -0
- package/dist/webrtc/index.d.ts.map +1 -1
- package/dist/webrtc/index.js +21 -7
- package/dist/websocket/client.d.ts.map +1 -1
- package/dist/websocket/client.js +13 -16
- package/package.json +2 -1
|
@@ -1,4 +1,83 @@
|
|
|
1
|
-
import { promises as dns } from 'node:dns';
|
|
1
|
+
import { promises as dns, Resolver, promises as dnsPromises } from 'node:dns';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { ProtocolError, UnsupportedError } from '../core/errors.js';
|
|
4
|
+
export async function dnsLookup(domain, type = 'A') {
|
|
5
|
+
const results = [];
|
|
6
|
+
const recordType = type.toUpperCase();
|
|
7
|
+
try {
|
|
8
|
+
switch (recordType) {
|
|
9
|
+
case 'A':
|
|
10
|
+
const a = await dns.resolve4(domain, { ttl: true });
|
|
11
|
+
return a.map(r => ({ type: 'A', ttl: r.ttl, data: r.address }));
|
|
12
|
+
case 'AAAA':
|
|
13
|
+
const aaaa = await dns.resolve6(domain, { ttl: true });
|
|
14
|
+
return aaaa.map(r => ({ type: 'AAAA', ttl: r.ttl, data: r.address }));
|
|
15
|
+
case 'CNAME':
|
|
16
|
+
const cname = await dns.resolveCname(domain);
|
|
17
|
+
return cname.map(r => ({ type: 'CNAME', data: r }));
|
|
18
|
+
case 'MX':
|
|
19
|
+
const mx = await dns.resolveMx(domain);
|
|
20
|
+
return mx.map(r => ({ type: 'MX', data: { priority: r.priority, exchange: r.exchange } }));
|
|
21
|
+
case 'NS':
|
|
22
|
+
const ns = await dns.resolveNs(domain);
|
|
23
|
+
return ns.map(r => ({ type: 'NS', data: r }));
|
|
24
|
+
case 'TXT':
|
|
25
|
+
const txt = await dns.resolveTxt(domain);
|
|
26
|
+
return txt.map(chunks => ({ type: 'TXT', data: chunks.join('') }));
|
|
27
|
+
case 'SOA':
|
|
28
|
+
const soa = await dns.resolveSoa(domain);
|
|
29
|
+
return [{ type: 'SOA', data: soa }];
|
|
30
|
+
case 'PTR':
|
|
31
|
+
const ptr = await dns.resolvePtr(domain);
|
|
32
|
+
return ptr.map(r => ({ type: 'PTR', data: r }));
|
|
33
|
+
case 'SRV':
|
|
34
|
+
const srv = await dns.resolveSrv(domain);
|
|
35
|
+
return srv.map(r => ({ type: 'SRV', data: r }));
|
|
36
|
+
case 'CAA':
|
|
37
|
+
const caa = await dns.resolveCaa(domain);
|
|
38
|
+
return caa.map(r => ({ type: 'CAA', data: r }));
|
|
39
|
+
case 'NAPTR':
|
|
40
|
+
const naptr = await dns.resolveNaptr(domain);
|
|
41
|
+
return naptr.map(r => ({ type: 'NAPTR', data: r }));
|
|
42
|
+
case 'ANY':
|
|
43
|
+
return dnsLookupAll(domain);
|
|
44
|
+
default:
|
|
45
|
+
throw new UnsupportedError(`Unsupported DNS record type: ${recordType}`, {
|
|
46
|
+
feature: recordType,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err.code === 'ENODATA' || err.code === 'ENOTFOUND') {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function dnsLookupAll(domain) {
|
|
58
|
+
const types = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SOA', 'CAA'];
|
|
59
|
+
const results = [];
|
|
60
|
+
await Promise.all(types.map(async (type) => {
|
|
61
|
+
try {
|
|
62
|
+
const records = await dnsLookup(domain, type);
|
|
63
|
+
results.push(...records);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
export async function reverseLookup(ip) {
|
|
71
|
+
try {
|
|
72
|
+
return await dns.reverse(ip);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err.code === 'ENOTFOUND') {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
2
81
|
export async function getSecurityRecords(domain) {
|
|
3
82
|
const results = { txt: [] };
|
|
4
83
|
try {
|
|
@@ -18,9 +97,7 @@ export async function getSecurityRecords(domain) {
|
|
|
18
97
|
try {
|
|
19
98
|
const caaRecords = await dns.resolveCaa(domain);
|
|
20
99
|
results.caa = {};
|
|
21
|
-
caaRecords.forEach(r => {
|
|
22
|
-
if (!results.caa[r.issue]) {
|
|
23
|
-
}
|
|
100
|
+
caaRecords.forEach((r) => {
|
|
24
101
|
if (r.issue) {
|
|
25
102
|
results.caa.issue = [...(results.caa.issue || []), r.issue];
|
|
26
103
|
}
|
|
@@ -36,13 +113,634 @@ export async function getSecurityRecords(domain) {
|
|
|
36
113
|
}
|
|
37
114
|
try {
|
|
38
115
|
const mx = await dns.resolveMx(domain);
|
|
39
|
-
results.mx = mx.map(r => r.exchange);
|
|
116
|
+
results.mx = mx.map(r => ({ priority: r.priority, exchange: r.exchange }));
|
|
40
117
|
}
|
|
41
118
|
catch {
|
|
42
119
|
}
|
|
43
120
|
}
|
|
44
121
|
catch (error) {
|
|
45
|
-
throw new
|
|
122
|
+
throw new ProtocolError(`Failed to resolve DNS for ${domain}: ${error}`, {
|
|
123
|
+
protocol: 'dns',
|
|
124
|
+
});
|
|
46
125
|
}
|
|
47
126
|
return results;
|
|
48
127
|
}
|
|
128
|
+
export async function validateSpf(domain) {
|
|
129
|
+
const result = {
|
|
130
|
+
valid: false,
|
|
131
|
+
mechanisms: [],
|
|
132
|
+
includes: [],
|
|
133
|
+
lookupCount: 0,
|
|
134
|
+
warnings: [],
|
|
135
|
+
errors: [],
|
|
136
|
+
};
|
|
137
|
+
try {
|
|
138
|
+
const txtRecords = await dns.resolveTxt(domain);
|
|
139
|
+
const spfRecords = txtRecords
|
|
140
|
+
.map(chunks => chunks.join(''))
|
|
141
|
+
.filter(txt => txt.startsWith('v=spf1'));
|
|
142
|
+
if (spfRecords.length === 0) {
|
|
143
|
+
result.errors.push('No SPF record found');
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
if (spfRecords.length > 1) {
|
|
147
|
+
result.errors.push('Multiple SPF records found (should have only one)');
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
result.record = spfRecords[0];
|
|
151
|
+
const parts = result.record.split(' ').slice(1);
|
|
152
|
+
for (const part of parts) {
|
|
153
|
+
if (part.startsWith('include:')) {
|
|
154
|
+
result.includes.push(part.replace('include:', ''));
|
|
155
|
+
result.lookupCount++;
|
|
156
|
+
}
|
|
157
|
+
else if (part.startsWith('redirect=')) {
|
|
158
|
+
result.lookupCount++;
|
|
159
|
+
}
|
|
160
|
+
else if (part.startsWith('a') || part.startsWith('mx') || part.startsWith('ptr')) {
|
|
161
|
+
result.lookupCount++;
|
|
162
|
+
}
|
|
163
|
+
result.mechanisms.push(part);
|
|
164
|
+
}
|
|
165
|
+
if (result.lookupCount > 10) {
|
|
166
|
+
result.errors.push(`Too many DNS lookups (${result.lookupCount}/10). SPF permerror will occur.`);
|
|
167
|
+
}
|
|
168
|
+
else if (result.lookupCount > 7) {
|
|
169
|
+
result.warnings.push(`High DNS lookup count (${result.lookupCount}/10). Consider flattening.`);
|
|
170
|
+
}
|
|
171
|
+
if (!result.record.includes('~all') && !result.record.includes('-all') && !result.record.includes('?all')) {
|
|
172
|
+
result.warnings.push('No "all" mechanism found. Consider adding -all or ~all');
|
|
173
|
+
}
|
|
174
|
+
if (result.record.includes('+all')) {
|
|
175
|
+
result.errors.push('Using +all allows anyone to send as your domain!');
|
|
176
|
+
}
|
|
177
|
+
result.valid = result.errors.length === 0;
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
result.errors.push(`DNS lookup failed: ${err.message}`);
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
export async function validateDmarc(domain) {
|
|
185
|
+
const result = {
|
|
186
|
+
valid: false,
|
|
187
|
+
policy: 'none',
|
|
188
|
+
percentage: 100,
|
|
189
|
+
warnings: [],
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
const txtRecords = await dns.resolveTxt(`_dmarc.${domain}`);
|
|
193
|
+
const dmarcRecord = txtRecords
|
|
194
|
+
.map(chunks => chunks.join(''))
|
|
195
|
+
.find(txt => txt.startsWith('v=DMARC1'));
|
|
196
|
+
if (!dmarcRecord) {
|
|
197
|
+
result.warnings.push('No DMARC record found');
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
result.record = dmarcRecord;
|
|
201
|
+
const tags = dmarcRecord.split(';').map(t => t.trim());
|
|
202
|
+
for (const tag of tags) {
|
|
203
|
+
const [key, value] = tag.split('=');
|
|
204
|
+
switch (key?.toLowerCase()) {
|
|
205
|
+
case 'p':
|
|
206
|
+
result.policy = value;
|
|
207
|
+
break;
|
|
208
|
+
case 'sp':
|
|
209
|
+
result.subdomainPolicy = value;
|
|
210
|
+
break;
|
|
211
|
+
case 'pct':
|
|
212
|
+
result.percentage = parseInt(value) || 100;
|
|
213
|
+
break;
|
|
214
|
+
case 'rua':
|
|
215
|
+
result.rua = value.split(',').map(v => v.trim());
|
|
216
|
+
break;
|
|
217
|
+
case 'ruf':
|
|
218
|
+
result.ruf = value.split(',').map(v => v.trim());
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (result.policy === 'none') {
|
|
223
|
+
result.warnings.push('DMARC policy is "none" - no emails will be rejected');
|
|
224
|
+
}
|
|
225
|
+
if (result.percentage < 100) {
|
|
226
|
+
result.warnings.push(`Only ${result.percentage}% of emails are subject to DMARC policy`);
|
|
227
|
+
}
|
|
228
|
+
if (!result.rua) {
|
|
229
|
+
result.warnings.push('No aggregate report (rua) recipients specified');
|
|
230
|
+
}
|
|
231
|
+
result.valid = true;
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (err.code !== 'ENODATA' && err.code !== 'ENOTFOUND') {
|
|
235
|
+
result.warnings.push(`DNS lookup failed: ${err.message}`);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
result.warnings.push('No DMARC record found');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
export async function checkDkim(domain, selector = 'default') {
|
|
244
|
+
try {
|
|
245
|
+
const dkimDomain = `${selector}._domainkey.${domain}`;
|
|
246
|
+
const txtRecords = await dns.resolveTxt(dkimDomain);
|
|
247
|
+
const record = txtRecords.map(chunks => chunks.join('')).find(txt => txt.includes('v=DKIM1'));
|
|
248
|
+
if (record) {
|
|
249
|
+
const pMatch = record.match(/p=([^;]+)/);
|
|
250
|
+
return {
|
|
251
|
+
found: true,
|
|
252
|
+
record,
|
|
253
|
+
publicKey: pMatch ? pMatch[1] : undefined,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return { found: false };
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return { found: false };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export async function checkDnsHealth(domain) {
|
|
263
|
+
const report = {
|
|
264
|
+
domain,
|
|
265
|
+
score: 0,
|
|
266
|
+
grade: 'F',
|
|
267
|
+
checks: [],
|
|
268
|
+
};
|
|
269
|
+
let maxScore = 0;
|
|
270
|
+
let earnedScore = 0;
|
|
271
|
+
maxScore += 10;
|
|
272
|
+
try {
|
|
273
|
+
const a = await dnsLookup(domain, 'A');
|
|
274
|
+
const aaaa = await dnsLookup(domain, 'AAAA');
|
|
275
|
+
if (a.length > 0 || aaaa.length > 0) {
|
|
276
|
+
earnedScore += 10;
|
|
277
|
+
report.checks.push({
|
|
278
|
+
name: 'A/AAAA Records',
|
|
279
|
+
status: 'pass',
|
|
280
|
+
message: `Found ${a.length} A and ${aaaa.length} AAAA records`,
|
|
281
|
+
details: { a: a.length, aaaa: aaaa.length },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
report.checks.push({
|
|
286
|
+
name: 'A/AAAA Records',
|
|
287
|
+
status: 'fail',
|
|
288
|
+
message: 'No A or AAAA records found',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
report.checks.push({
|
|
294
|
+
name: 'A/AAAA Records',
|
|
295
|
+
status: 'fail',
|
|
296
|
+
message: 'Failed to resolve A/AAAA records',
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
maxScore += 10;
|
|
300
|
+
try {
|
|
301
|
+
const ns = await dnsLookup(domain, 'NS');
|
|
302
|
+
if (ns.length >= 2) {
|
|
303
|
+
earnedScore += 10;
|
|
304
|
+
report.checks.push({
|
|
305
|
+
name: 'Nameservers',
|
|
306
|
+
status: 'pass',
|
|
307
|
+
message: `Found ${ns.length} nameservers (redundancy OK)`,
|
|
308
|
+
details: ns.map(n => n.data),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else if (ns.length === 1) {
|
|
312
|
+
earnedScore += 5;
|
|
313
|
+
report.checks.push({
|
|
314
|
+
name: 'Nameservers',
|
|
315
|
+
status: 'warn',
|
|
316
|
+
message: 'Only 1 nameserver found. Consider adding redundancy.',
|
|
317
|
+
details: ns.map(n => n.data),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
report.checks.push({
|
|
322
|
+
name: 'Nameservers',
|
|
323
|
+
status: 'fail',
|
|
324
|
+
message: 'No nameservers found',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
report.checks.push({
|
|
330
|
+
name: 'Nameservers',
|
|
331
|
+
status: 'fail',
|
|
332
|
+
message: 'Failed to resolve NS records',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
maxScore += 5;
|
|
336
|
+
try {
|
|
337
|
+
const soa = await dnsLookup(domain, 'SOA');
|
|
338
|
+
if (soa.length > 0) {
|
|
339
|
+
earnedScore += 5;
|
|
340
|
+
report.checks.push({
|
|
341
|
+
name: 'SOA Record',
|
|
342
|
+
status: 'pass',
|
|
343
|
+
message: 'SOA record present',
|
|
344
|
+
details: soa[0].data,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
report.checks.push({
|
|
350
|
+
name: 'SOA Record',
|
|
351
|
+
status: 'warn',
|
|
352
|
+
message: 'No SOA record found',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
maxScore += 15;
|
|
356
|
+
const spf = await validateSpf(domain);
|
|
357
|
+
if (spf.valid) {
|
|
358
|
+
earnedScore += 15;
|
|
359
|
+
report.checks.push({
|
|
360
|
+
name: 'SPF Record',
|
|
361
|
+
status: 'pass',
|
|
362
|
+
message: 'Valid SPF record found',
|
|
363
|
+
details: { record: spf.record, lookups: spf.lookupCount },
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
else if (spf.record) {
|
|
367
|
+
earnedScore += 7;
|
|
368
|
+
report.checks.push({
|
|
369
|
+
name: 'SPF Record',
|
|
370
|
+
status: 'warn',
|
|
371
|
+
message: spf.errors[0] || spf.warnings[0] || 'SPF has issues',
|
|
372
|
+
details: { record: spf.record, errors: spf.errors, warnings: spf.warnings },
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
report.checks.push({
|
|
377
|
+
name: 'SPF Record',
|
|
378
|
+
status: 'fail',
|
|
379
|
+
message: 'No SPF record found. Email spoofing possible.',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
maxScore += 15;
|
|
383
|
+
const dmarc = await validateDmarc(domain);
|
|
384
|
+
if (dmarc.valid && dmarc.policy !== 'none') {
|
|
385
|
+
earnedScore += 15;
|
|
386
|
+
report.checks.push({
|
|
387
|
+
name: 'DMARC Record',
|
|
388
|
+
status: 'pass',
|
|
389
|
+
message: `DMARC policy: ${dmarc.policy}`,
|
|
390
|
+
details: { policy: dmarc.policy, percentage: dmarc.percentage },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
else if (dmarc.valid) {
|
|
394
|
+
earnedScore += 7;
|
|
395
|
+
report.checks.push({
|
|
396
|
+
name: 'DMARC Record',
|
|
397
|
+
status: 'warn',
|
|
398
|
+
message: 'DMARC exists but policy is "none"',
|
|
399
|
+
details: dmarc,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
report.checks.push({
|
|
404
|
+
name: 'DMARC Record',
|
|
405
|
+
status: 'fail',
|
|
406
|
+
message: 'No DMARC record found',
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
maxScore += 10;
|
|
410
|
+
try {
|
|
411
|
+
const mx = await dnsLookup(domain, 'MX');
|
|
412
|
+
if (mx.length > 0) {
|
|
413
|
+
earnedScore += 10;
|
|
414
|
+
report.checks.push({
|
|
415
|
+
name: 'MX Records',
|
|
416
|
+
status: 'pass',
|
|
417
|
+
message: `Found ${mx.length} mail server(s)`,
|
|
418
|
+
details: mx.map(m => m.data),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
report.checks.push({
|
|
423
|
+
name: 'MX Records',
|
|
424
|
+
status: 'warn',
|
|
425
|
+
message: 'No MX records found (domain cannot receive email)',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
report.checks.push({
|
|
431
|
+
name: 'MX Records',
|
|
432
|
+
status: 'warn',
|
|
433
|
+
message: 'No MX records found',
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
maxScore += 10;
|
|
437
|
+
try {
|
|
438
|
+
const caa = await dnsLookup(domain, 'CAA');
|
|
439
|
+
if (caa.length > 0) {
|
|
440
|
+
earnedScore += 10;
|
|
441
|
+
report.checks.push({
|
|
442
|
+
name: 'CAA Records',
|
|
443
|
+
status: 'pass',
|
|
444
|
+
message: 'CAA records configured (certificate issuance restricted)',
|
|
445
|
+
details: caa.map(c => c.data),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
earnedScore += 3;
|
|
450
|
+
report.checks.push({
|
|
451
|
+
name: 'CAA Records',
|
|
452
|
+
status: 'warn',
|
|
453
|
+
message: 'No CAA records. Any CA can issue certificates for this domain.',
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
report.checks.push({
|
|
459
|
+
name: 'CAA Records',
|
|
460
|
+
status: 'warn',
|
|
461
|
+
message: 'No CAA records found',
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
report.score = Math.round((earnedScore / maxScore) * 100);
|
|
465
|
+
if (report.score >= 90)
|
|
466
|
+
report.grade = 'A';
|
|
467
|
+
else if (report.score >= 80)
|
|
468
|
+
report.grade = 'B';
|
|
469
|
+
else if (report.score >= 70)
|
|
470
|
+
report.grade = 'C';
|
|
471
|
+
else if (report.score >= 60)
|
|
472
|
+
report.grade = 'D';
|
|
473
|
+
else
|
|
474
|
+
report.grade = 'F';
|
|
475
|
+
return report;
|
|
476
|
+
}
|
|
477
|
+
export function generateDmarc(options) {
|
|
478
|
+
const parts = ['v=DMARC1', `p=${options.policy}`];
|
|
479
|
+
if (options.subdomainPolicy && options.subdomainPolicy !== options.policy) {
|
|
480
|
+
parts.push(`sp=${options.subdomainPolicy}`);
|
|
481
|
+
}
|
|
482
|
+
if (options.percentage !== undefined && options.percentage !== 100) {
|
|
483
|
+
parts.push(`pct=${options.percentage}`);
|
|
484
|
+
}
|
|
485
|
+
if (options.aggregateReports && options.aggregateReports.length > 0) {
|
|
486
|
+
parts.push(`rua=${options.aggregateReports.map(e => `mailto:${e}`).join(',')}`);
|
|
487
|
+
}
|
|
488
|
+
if (options.forensicReports && options.forensicReports.length > 0) {
|
|
489
|
+
parts.push(`ruf=${options.forensicReports.map(e => `mailto:${e}`).join(',')}`);
|
|
490
|
+
}
|
|
491
|
+
if (options.alignmentDkim === 'strict') {
|
|
492
|
+
parts.push('adkim=s');
|
|
493
|
+
}
|
|
494
|
+
if (options.alignmentSpf === 'strict') {
|
|
495
|
+
parts.push('aspf=s');
|
|
496
|
+
}
|
|
497
|
+
if (options.reportInterval && options.reportInterval !== 86400) {
|
|
498
|
+
parts.push(`ri=${options.reportInterval}`);
|
|
499
|
+
}
|
|
500
|
+
if (options.failureOptions) {
|
|
501
|
+
parts.push(`fo=${options.failureOptions}`);
|
|
502
|
+
}
|
|
503
|
+
return parts.join('; ');
|
|
504
|
+
}
|
|
505
|
+
function getResolver(server) {
|
|
506
|
+
if (!server) {
|
|
507
|
+
return dnsPromises;
|
|
508
|
+
}
|
|
509
|
+
const resolver = new Resolver();
|
|
510
|
+
const [host, port] = server.split(':');
|
|
511
|
+
resolver.setServers([port ? `${host}:${port}` : host]);
|
|
512
|
+
return {
|
|
513
|
+
resolve4: promisify(resolver.resolve4.bind(resolver)),
|
|
514
|
+
resolve6: promisify(resolver.resolve6.bind(resolver)),
|
|
515
|
+
resolveMx: promisify(resolver.resolveMx.bind(resolver)),
|
|
516
|
+
resolveNs: promisify(resolver.resolveNs.bind(resolver)),
|
|
517
|
+
resolveTxt: promisify(resolver.resolveTxt.bind(resolver)),
|
|
518
|
+
resolveCname: promisify(resolver.resolveCname.bind(resolver)),
|
|
519
|
+
resolveSoa: promisify(resolver.resolveSoa.bind(resolver)),
|
|
520
|
+
resolvePtr: promisify(resolver.resolvePtr.bind(resolver)),
|
|
521
|
+
resolveSrv: promisify(resolver.resolveSrv.bind(resolver)),
|
|
522
|
+
resolveCaa: promisify(resolver.resolveCaa.bind(resolver)),
|
|
523
|
+
resolveNaptr: promisify(resolver.resolveNaptr.bind(resolver)),
|
|
524
|
+
reverse: promisify(resolver.reverse.bind(resolver)),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function formatDigData(type, data) {
|
|
528
|
+
if (typeof data === 'string')
|
|
529
|
+
return data;
|
|
530
|
+
switch (type) {
|
|
531
|
+
case 'MX':
|
|
532
|
+
return `${data.priority} ${data.exchange}`;
|
|
533
|
+
case 'SOA':
|
|
534
|
+
return `${data.nsname} ${data.hostmaster} ${data.serial} ${data.refresh} ${data.retry} ${data.expire} ${data.minttl}`;
|
|
535
|
+
case 'SRV':
|
|
536
|
+
return `${data.priority} ${data.weight} ${data.port} ${data.name}`;
|
|
537
|
+
case 'CAA':
|
|
538
|
+
return `${data.critical} ${data.issue || data.issuewild || data.iodef || ''}`;
|
|
539
|
+
case 'NAPTR':
|
|
540
|
+
return `${data.order} ${data.preference} "${data.flags}" "${data.service}" "${data.regexp}" ${data.replacement}`;
|
|
541
|
+
default:
|
|
542
|
+
return JSON.stringify(data);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
export async function dig(domain, options = {}) {
|
|
546
|
+
const startTime = performance.now();
|
|
547
|
+
const type = (options.type || 'A').toUpperCase();
|
|
548
|
+
const resolver = getResolver(options.server);
|
|
549
|
+
const serverName = options.server || 'system-default';
|
|
550
|
+
const result = {
|
|
551
|
+
question: {
|
|
552
|
+
name: domain,
|
|
553
|
+
type,
|
|
554
|
+
class: 'IN',
|
|
555
|
+
},
|
|
556
|
+
answer: [],
|
|
557
|
+
server: serverName,
|
|
558
|
+
queryTime: 0,
|
|
559
|
+
when: new Date(),
|
|
560
|
+
};
|
|
561
|
+
try {
|
|
562
|
+
if (options.reverse) {
|
|
563
|
+
const hostnames = await resolver.reverse(domain);
|
|
564
|
+
result.question.type = 'PTR';
|
|
565
|
+
result.answer = hostnames.map(hostname => ({
|
|
566
|
+
name: domain,
|
|
567
|
+
type: 'PTR',
|
|
568
|
+
class: 'IN',
|
|
569
|
+
ttl: 0,
|
|
570
|
+
data: hostname,
|
|
571
|
+
}));
|
|
572
|
+
result.queryTime = Math.round(performance.now() - startTime);
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
switch (type) {
|
|
576
|
+
case 'A': {
|
|
577
|
+
const records = await resolver.resolve4(domain, { ttl: true });
|
|
578
|
+
result.answer = records.map(r => ({
|
|
579
|
+
name: domain,
|
|
580
|
+
type: 'A',
|
|
581
|
+
class: 'IN',
|
|
582
|
+
ttl: r.ttl,
|
|
583
|
+
data: r.address,
|
|
584
|
+
}));
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case 'AAAA': {
|
|
588
|
+
const records = await resolver.resolve6(domain, { ttl: true });
|
|
589
|
+
result.answer = records.map(r => ({
|
|
590
|
+
name: domain,
|
|
591
|
+
type: 'AAAA',
|
|
592
|
+
class: 'IN',
|
|
593
|
+
ttl: r.ttl,
|
|
594
|
+
data: r.address,
|
|
595
|
+
}));
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
case 'MX': {
|
|
599
|
+
const records = await resolver.resolveMx(domain);
|
|
600
|
+
result.answer = records.map(r => ({
|
|
601
|
+
name: domain,
|
|
602
|
+
type: 'MX',
|
|
603
|
+
class: 'IN',
|
|
604
|
+
ttl: 0,
|
|
605
|
+
data: formatDigData('MX', r),
|
|
606
|
+
}));
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case 'NS': {
|
|
610
|
+
const records = await resolver.resolveNs(domain);
|
|
611
|
+
result.answer = records.map(r => ({
|
|
612
|
+
name: domain,
|
|
613
|
+
type: 'NS',
|
|
614
|
+
class: 'IN',
|
|
615
|
+
ttl: 0,
|
|
616
|
+
data: r,
|
|
617
|
+
}));
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case 'TXT': {
|
|
621
|
+
const records = await resolver.resolveTxt(domain);
|
|
622
|
+
result.answer = records.map(chunks => ({
|
|
623
|
+
name: domain,
|
|
624
|
+
type: 'TXT',
|
|
625
|
+
class: 'IN',
|
|
626
|
+
ttl: 0,
|
|
627
|
+
data: `"${chunks.join('')}"`,
|
|
628
|
+
}));
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case 'CNAME': {
|
|
632
|
+
const records = await resolver.resolveCname(domain);
|
|
633
|
+
result.answer = records.map(r => ({
|
|
634
|
+
name: domain,
|
|
635
|
+
type: 'CNAME',
|
|
636
|
+
class: 'IN',
|
|
637
|
+
ttl: 0,
|
|
638
|
+
data: r,
|
|
639
|
+
}));
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case 'SOA': {
|
|
643
|
+
const record = await resolver.resolveSoa(domain);
|
|
644
|
+
result.answer = [{
|
|
645
|
+
name: domain,
|
|
646
|
+
type: 'SOA',
|
|
647
|
+
class: 'IN',
|
|
648
|
+
ttl: 0,
|
|
649
|
+
data: formatDigData('SOA', record),
|
|
650
|
+
}];
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
case 'PTR': {
|
|
654
|
+
const records = await resolver.resolvePtr(domain);
|
|
655
|
+
result.answer = records.map(r => ({
|
|
656
|
+
name: domain,
|
|
657
|
+
type: 'PTR',
|
|
658
|
+
class: 'IN',
|
|
659
|
+
ttl: 0,
|
|
660
|
+
data: r,
|
|
661
|
+
}));
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
case 'SRV': {
|
|
665
|
+
const records = await resolver.resolveSrv(domain);
|
|
666
|
+
result.answer = records.map(r => ({
|
|
667
|
+
name: domain,
|
|
668
|
+
type: 'SRV',
|
|
669
|
+
class: 'IN',
|
|
670
|
+
ttl: 0,
|
|
671
|
+
data: formatDigData('SRV', r),
|
|
672
|
+
}));
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
case 'CAA': {
|
|
676
|
+
const records = await resolver.resolveCaa(domain);
|
|
677
|
+
result.answer = records.map(r => ({
|
|
678
|
+
name: domain,
|
|
679
|
+
type: 'CAA',
|
|
680
|
+
class: 'IN',
|
|
681
|
+
ttl: 0,
|
|
682
|
+
data: formatDigData('CAA', r),
|
|
683
|
+
}));
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case 'NAPTR': {
|
|
687
|
+
const records = await resolver.resolveNaptr(domain);
|
|
688
|
+
result.answer = records.map(r => ({
|
|
689
|
+
name: domain,
|
|
690
|
+
type: 'NAPTR',
|
|
691
|
+
class: 'IN',
|
|
692
|
+
ttl: 0,
|
|
693
|
+
data: formatDigData('NAPTR', r),
|
|
694
|
+
}));
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case 'ANY': {
|
|
698
|
+
const types = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'SOA', 'CAA'];
|
|
699
|
+
for (const t of types) {
|
|
700
|
+
try {
|
|
701
|
+
const subResult = await dig(domain, { ...options, type: t });
|
|
702
|
+
result.answer.push(...subResult.answer);
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
default:
|
|
710
|
+
throw new UnsupportedError(`Unsupported DNS record type: ${type}`, {
|
|
711
|
+
feature: type,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
if (err.code !== 'ENODATA' && err.code !== 'ENOTFOUND') {
|
|
717
|
+
throw err;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
result.queryTime = Math.round(performance.now() - startTime);
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
export function formatDigOutput(result, short = false) {
|
|
724
|
+
if (short) {
|
|
725
|
+
return result.answer.map(a => a.data).join('\n');
|
|
726
|
+
}
|
|
727
|
+
const lines = [];
|
|
728
|
+
lines.push(`; <<>> rek dig <<>> ${result.question.name} ${result.question.type}`);
|
|
729
|
+
lines.push(`;; Got answer:`);
|
|
730
|
+
lines.push('');
|
|
731
|
+
lines.push(';; QUESTION SECTION:');
|
|
732
|
+
lines.push(`;${result.question.name.padEnd(23)} ${result.question.class.padEnd(4)} ${result.question.type}`);
|
|
733
|
+
lines.push('');
|
|
734
|
+
if (result.answer.length > 0) {
|
|
735
|
+
lines.push(';; ANSWER SECTION:');
|
|
736
|
+
for (const answer of result.answer) {
|
|
737
|
+
const ttl = answer.ttl.toString().padStart(5);
|
|
738
|
+
lines.push(`${answer.name.padEnd(23)} ${ttl} ${answer.class.padEnd(4)} ${answer.type.padEnd(6)} ${answer.data}`);
|
|
739
|
+
}
|
|
740
|
+
lines.push('');
|
|
741
|
+
}
|
|
742
|
+
lines.push(`;; Query time: ${result.queryTime} msec`);
|
|
743
|
+
lines.push(`;; SERVER: ${result.server}`);
|
|
744
|
+
lines.push(`;; WHEN: ${result.when.toUTCString()}`);
|
|
745
|
+
return lines.join('\n');
|
|
746
|
+
}
|