@aiwg/cli 2026.9.4 → 2026.9.6
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/THIRD_PARTY_NOTICES.md +16 -0
- package/agentic/code/providers/capability-matrix.yaml +45 -3
- package/agentic/code/providers/deepseek-harness/README.md +8 -0
- package/agentic/code/providers/deepseek-harness/aiwg.cordis.patch.yml +10 -0
- package/agentic/code/providers/model-capabilities.v1.json +11 -0
- package/agentic/code/providers/model-catalog.v1.json +8 -0
- package/bin/aiwg.mjs +1 -0
- package/dist/src/api/index.d.ts +14 -0
- package/dist/src/api/index.js +14 -0
- package/dist/src/artifacts/corpus-tools/source-types.js +1 -0
- package/dist/src/artifacts/index-files.js +17 -2
- package/dist/src/artifacts/repair.js +55 -6
- package/dist/src/catalog/cli.js +21 -7
- package/dist/src/catalog/cli.mjs +22 -7
- package/dist/src/cli/agent-spawn.js +10 -1
- package/dist/src/cli/handlers/artifacts.js +22 -3
- package/dist/src/cli/handlers/help.js +3 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/models.js +2 -2
- package/dist/src/cli/handlers/output-mode.js +1 -1
- package/dist/src/cli/handlers/runtime-info.js +1 -1
- package/dist/src/cli/handlers/sessions.js +55 -15
- package/dist/src/cli/handlers/steward.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +5 -0
- package/dist/src/cli/handlers/writer-profile.js +110 -0
- package/dist/src/cli/handlers/writing.js +122 -0
- package/dist/src/cli/router.js +5 -1
- package/dist/src/config/project-artifacts-runtime.mjs +33 -1
- package/dist/src/config/project-artifacts.js +1 -1
- package/dist/src/dataset/fortemi-dataset-execution.d.ts +23 -0
- package/dist/src/dataset/fortemi-dataset-execution.js +158 -0
- package/dist/src/dataset/fortemi-live-qualification.d.ts +4 -2
- package/dist/src/dataset/fortemi-live-qualification.js +20 -21
- package/dist/src/dataset/fortemi-run-receipt.d.ts +44 -0
- package/dist/src/dataset/fortemi-run-receipt.js +74 -0
- package/dist/src/dataset/index.d.ts +2 -0
- package/dist/src/dataset/index.js +2 -0
- package/dist/src/extensions/commands/definitions.js +24 -0
- package/dist/src/extensions/manifest.js +3 -0
- package/dist/src/mcp/server.mjs +2 -0
- package/dist/src/mcp/tools/writer-profiles.mjs +40 -0
- package/dist/src/models/model-capabilities.v1.json +11 -0
- package/dist/src/models/model-catalog.v1.json +8 -0
- package/dist/src/models/provider-policy.js +1 -1
- package/dist/src/network-analysis/analyzer.js +667 -0
- package/dist/src/network-analysis/citations.js +107 -0
- package/dist/src/network-analysis/forensics.js +132 -0
- package/dist/src/network-analysis/governance.js +216 -0
- package/dist/src/network-analysis/index.js +10 -0
- package/dist/src/network-analysis/probe.js +405 -0
- package/dist/src/network-analysis/recipes.js +88 -0
- package/dist/src/network-analysis/research.js +181 -0
- package/dist/src/network-analysis/termshark.js +252 -0
- package/dist/src/network-analysis/verification.js +171 -0
- package/dist/src/output-modes/registry.js +37 -6
- package/dist/src/output-modes/runtime.js +164 -28
- package/dist/src/providers/capability-matrix.yaml +45 -3
- package/dist/src/providers/provider-definitions.js +49 -0
- package/dist/src/providers/provider-inventory.js +1 -0
- package/dist/src/providers/transformation-receipt.js +3 -2
- package/dist/src/sessions/adapters/deepseek-harness.js +178 -0
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/contracts.js +2 -1
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/workspace-discovery.js +5 -1
- package/dist/src/skills/deployer.js +6 -6
- package/dist/src/smiths/context-pipeline/workspace-context.js +7 -0
- package/dist/src/writing/channel-packs.js +13 -0
- package/dist/src/writing/contextual-diagnostics.js +142 -0
- package/dist/src/writing/example-generator.js +7 -6
- package/dist/src/writing/exemplar-selection.js +186 -0
- package/dist/src/writing/fidelity.js +61 -0
- package/dist/src/writing/validation-engine.js +32 -15
- package/dist/src/writing/voice-evaluation.js +301 -0
- package/dist/src/writing/voice-revision.js +201 -0
- package/dist/src/writing/writer-migration.js +216 -0
- package/dist/src/writing/writer-profile-legacy.js +145 -0
- package/dist/src/writing/writer-profile-store.js +117 -0
- package/dist/src/writing/writer-profile.js +222 -0
- package/dist/src/writing/writing-brief.js +166 -0
- package/dist/src/writing/writing-channels.js +63 -0
- package/dist/src/writing/writing-consumer.js +39 -0
- package/dist/src/writing/writing-receipt.js +266 -0
- package/package.json +2 -1
- package/schemas/dataset/fortemi-live-qualification-receipt.v2.schema.json +196 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/authority.json +12 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/run-receipt.schema.json +819 -0
- package/tools/agents/deploy-agents.mjs +7 -3
- package/tools/agents/providers/antigravity.mjs +1 -1
- package/tools/agents/providers/base.mjs +3 -2
- package/tools/agents/providers/deepseek-harness.mjs +66 -0
- package/tools/agents/providers/hermes.mjs +1 -1
- package/tools/agents/providers/openhuman.mjs +2 -2
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { constants, lstat, mkdtemp, open, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { hashEvidenceFile, resolveCaptureDestination, safeProcessSpec, tsharkFilterArgs, verifyEvidenceFile, } from './governance.js';
|
|
8
|
+
const execFile = promisify(execFileCallback);
|
|
9
|
+
export const PACKET_EVIDENCE_SCHEMA_VERSION = 'network-analysis.packet-evidence/v1';
|
|
10
|
+
const HARD_INPUT_LIMIT = 2 * 1024 * 1024 * 1024;
|
|
11
|
+
const HARD_PACKET_LIMIT = 1_000_000;
|
|
12
|
+
const HARD_OUTPUT_LIMIT = 128 * 1024 * 1024;
|
|
13
|
+
const HARD_TIMEOUT_MS = 60 * 60 * 1000;
|
|
14
|
+
const COMPRESSED_EXTENSIONS = /\.(?:gz|bz2|xz|zip|zst|7z|rar)$/i;
|
|
15
|
+
const SAFE_FIELD = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
16
|
+
const BLOCKED_FIELD = /(?:payload|raw|cookie|authorization|credential|passwd|password|secret|token|keylog|file_data|data\.data|reassembled\.data)/i;
|
|
17
|
+
export const DEFAULT_METADATA_FIELDS = Object.freeze([
|
|
18
|
+
'frame.number',
|
|
19
|
+
'frame.time_epoch',
|
|
20
|
+
'frame.len',
|
|
21
|
+
'_ws.col.Protocol',
|
|
22
|
+
'eth.src',
|
|
23
|
+
'eth.dst',
|
|
24
|
+
'ip.src',
|
|
25
|
+
'ip.dst',
|
|
26
|
+
'ipv6.src',
|
|
27
|
+
'ipv6.dst',
|
|
28
|
+
'tcp.srcport',
|
|
29
|
+
'tcp.dstport',
|
|
30
|
+
'tcp.stream',
|
|
31
|
+
'tcp.analysis.retransmission',
|
|
32
|
+
'tcp.flags.reset',
|
|
33
|
+
'udp.srcport',
|
|
34
|
+
'udp.dstport',
|
|
35
|
+
'udp.stream',
|
|
36
|
+
'dns.qry.name',
|
|
37
|
+
'dns.flags.response',
|
|
38
|
+
'tls.handshake.type',
|
|
39
|
+
'tls.handshake.extensions_server_name',
|
|
40
|
+
'http.request.method',
|
|
41
|
+
'http.host',
|
|
42
|
+
'http.response.code',
|
|
43
|
+
]);
|
|
44
|
+
export function createDefaultAnalyzerHost() {
|
|
45
|
+
return {
|
|
46
|
+
async run(file, args, limits) {
|
|
47
|
+
try {
|
|
48
|
+
const result = await execFile(file, [...args], {
|
|
49
|
+
timeout: limits.timeoutMs,
|
|
50
|
+
maxBuffer: limits.maxBufferBytes,
|
|
51
|
+
killSignal: 'SIGKILL',
|
|
52
|
+
signal: limits.signal,
|
|
53
|
+
shell: false,
|
|
54
|
+
env: isolatedAnalyzerEnv(limits.isolatedConfigRoot),
|
|
55
|
+
});
|
|
56
|
+
return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const cancelled = error?.name === 'AbortError' || error?.code === 'ABORT_ERR';
|
|
60
|
+
const outputLimited = error?.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
|
|
61
|
+
return {
|
|
62
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
63
|
+
stdout: typeof error?.stdout === 'string' ? error.stdout : '',
|
|
64
|
+
stderr: sanitizeDiagnostic(typeof error?.stderr === 'string' ? error.stderr : String(error?.message ?? '')),
|
|
65
|
+
timedOut: !cancelled && !outputLimited && error?.killed === true,
|
|
66
|
+
outputLimited,
|
|
67
|
+
cancelled,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export async function analyzeOfflineCapture(options, host = createDefaultAnalyzerHost()) {
|
|
74
|
+
const limits = normalizeLimits(options.limits);
|
|
75
|
+
const fields = normalizeFields(options.recipe.fields);
|
|
76
|
+
const filterArgs = tsharkFilterArgs(options.recipe.displayFilter === undefined ? {} : {
|
|
77
|
+
displayFilter: {
|
|
78
|
+
type: 'display_filter',
|
|
79
|
+
language: 'wireshark-display',
|
|
80
|
+
expression: options.recipe.displayFilter,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
validateOptions(options);
|
|
84
|
+
const started = (options.now ?? (() => new Date()))().toISOString();
|
|
85
|
+
const workspace = await mkdtemp(path.join(tmpdir(), 'aiwg-network-analysis-'));
|
|
86
|
+
let sourceIdentity;
|
|
87
|
+
let snapshot;
|
|
88
|
+
try {
|
|
89
|
+
sourceIdentity = await validateAndHashInput(options.capturePath, limits.inputBytes);
|
|
90
|
+
snapshot = await createVerifiedSnapshot(options.capturePath, workspace, sourceIdentity);
|
|
91
|
+
const canonicalArgs = buildTsharkArgs(`pcap:sha256:${sourceIdentity.value}`, limits.packets, filterArgs, fields);
|
|
92
|
+
const executionArgs = buildTsharkArgs(snapshot.path, limits.packets, filterArgs, fields);
|
|
93
|
+
const processSpec = safeProcessSpec(options.tshark.path, executionArgs);
|
|
94
|
+
const context = analysisContext(options, canonicalArgs, fields);
|
|
95
|
+
const result = await host.run(processSpec.file, processSpec.args, {
|
|
96
|
+
timeoutMs: limits.timeoutMs,
|
|
97
|
+
maxBufferBytes: limits.outputBytes,
|
|
98
|
+
signal: options.signal,
|
|
99
|
+
isolatedConfigRoot: workspace,
|
|
100
|
+
});
|
|
101
|
+
const parsed = parseTsharkJson(result.stdout, fields, limits.packets, result.cancelled === true || result.timedOut === true || result.outputLimited === true || result.exitCode !== 0);
|
|
102
|
+
await verifyEvidenceFile(options.capturePath, sourceIdentity);
|
|
103
|
+
const errors = classifyProcessResult(result, parsed.length);
|
|
104
|
+
const status = result.cancelled || result.timedOut || result.outputLimited
|
|
105
|
+
? 'partial'
|
|
106
|
+
: result.exitCode !== 0
|
|
107
|
+
? (parsed.length > 0 ? 'partial' : 'error')
|
|
108
|
+
: parsed.length === 0 ? 'empty' : 'completed';
|
|
109
|
+
return buildBundle({
|
|
110
|
+
options,
|
|
111
|
+
sourceIdentity,
|
|
112
|
+
snapshot,
|
|
113
|
+
context,
|
|
114
|
+
fields,
|
|
115
|
+
packets: parsed,
|
|
116
|
+
errors,
|
|
117
|
+
status,
|
|
118
|
+
started,
|
|
119
|
+
ended: (options.now ?? (() => new Date()))().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (!sourceIdentity)
|
|
124
|
+
throw error;
|
|
125
|
+
const fallbackFormat = snapshot?.format ?? detectFormat(await readPrefix(options.capturePath));
|
|
126
|
+
const canonicalArgs = buildTsharkArgs(`pcap:sha256:${sourceIdentity.value}`, limits.packets, filterArgs, fields);
|
|
127
|
+
return buildBundle({
|
|
128
|
+
options,
|
|
129
|
+
sourceIdentity,
|
|
130
|
+
snapshot: { path: '', format: fallbackFormat ?? 'unknown', identity: sourceIdentity },
|
|
131
|
+
context: analysisContext(options, canonicalArgs, fields),
|
|
132
|
+
fields,
|
|
133
|
+
packets: [],
|
|
134
|
+
errors: [{ code: errorCode(error), message: sanitizeDiagnostic(String(error?.message ?? error)), severity: 'error' }],
|
|
135
|
+
status: 'error',
|
|
136
|
+
started,
|
|
137
|
+
ended: (options.now ?? (() => new Date()))().toISOString(),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
await rm(workspace, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function writePacketEvidence(bundle, output) {
|
|
145
|
+
const destination = await resolveCaptureDestination(output.root, output.path);
|
|
146
|
+
const bytes = serializePacketEvidence(bundle, output.format);
|
|
147
|
+
const handle = await open(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
148
|
+
try {
|
|
149
|
+
await handle.writeFile(bytes, 'utf8');
|
|
150
|
+
await handle.sync();
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await handle.close();
|
|
154
|
+
}
|
|
155
|
+
return { path: destination, format: output.format, identity: await hashEvidenceFile(destination, 'derived-artifact') };
|
|
156
|
+
}
|
|
157
|
+
export function serializePacketEvidence(bundle, format) {
|
|
158
|
+
if (format === 'json')
|
|
159
|
+
return `${canonicalJson(bundle, 2)}\n`;
|
|
160
|
+
if (format === 'jsonl')
|
|
161
|
+
return `${canonicalJson(bundle)}\n`;
|
|
162
|
+
const digest = String(bundle.capture.hashes.source.value);
|
|
163
|
+
const lines = [
|
|
164
|
+
'# Packet evidence',
|
|
165
|
+
'',
|
|
166
|
+
`- Bundle: ${bundle.bundle_id}`,
|
|
167
|
+
`- Status: ${bundle.status}`,
|
|
168
|
+
`- Capture: sha256:${digest}`,
|
|
169
|
+
`- Recipe: ${String(bundle.provenance.recipe_id)}`,
|
|
170
|
+
`- Evidence items: ${bundle.evidence_items.length}`,
|
|
171
|
+
'',
|
|
172
|
+
'## Observations',
|
|
173
|
+
'',
|
|
174
|
+
];
|
|
175
|
+
for (const item of bundle.evidence_items) {
|
|
176
|
+
const citation = item.citations[0];
|
|
177
|
+
lines.push(`- ${String(item.statement)} (frame ${String(citation.locator.frame_number)})`);
|
|
178
|
+
}
|
|
179
|
+
if (bundle.errors.length > 0) {
|
|
180
|
+
lines.push('', '## Errors', '');
|
|
181
|
+
for (const error of bundle.errors)
|
|
182
|
+
lines.push(`- ${error.code}: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
return `${lines.join('\n')}\n`;
|
|
185
|
+
}
|
|
186
|
+
async function validateAndHashInput(file, maxBytes) {
|
|
187
|
+
if (!path.isAbsolute(file))
|
|
188
|
+
throw coded('CAPTURE_PATH_INVALID', 'Capture path must be absolute.');
|
|
189
|
+
if (COMPRESSED_EXTENSIONS.test(file))
|
|
190
|
+
throw coded('CAPTURE_COMPRESSED', 'Compressed capture input is not accepted; decompress it into the authorized workspace first.');
|
|
191
|
+
const stat = await lstat(file);
|
|
192
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
193
|
+
throw coded('CAPTURE_PATH_INVALID', 'Capture input must be a regular non-symlink file.');
|
|
194
|
+
if (stat.size > maxBytes)
|
|
195
|
+
throw coded('CAPTURE_OVERSIZED', `Capture exceeds the configured ${maxBytes}-byte input limit.`);
|
|
196
|
+
return hashEvidenceFile(file, 'source-capture');
|
|
197
|
+
}
|
|
198
|
+
async function createVerifiedSnapshot(source, workspace, expected) {
|
|
199
|
+
const prefix = await readPrefix(source);
|
|
200
|
+
const format = detectFormat(prefix);
|
|
201
|
+
if (!format) {
|
|
202
|
+
if (prefix.length === 0)
|
|
203
|
+
throw coded('CAPTURE_EMPTY', 'Capture is empty.');
|
|
204
|
+
throw coded('CAPTURE_UNSUPPORTED', 'Capture magic is not a supported PCAP or PCAPNG format.');
|
|
205
|
+
}
|
|
206
|
+
const minimum = format === 'pcap' ? 24 : 28;
|
|
207
|
+
if (expected.byteLength < minimum)
|
|
208
|
+
throw coded('CAPTURE_TRUNCATED', `${format.toUpperCase()} input is shorter than its minimum header.`);
|
|
209
|
+
const sourceHandle = await open(source, constants.O_RDONLY | (typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0));
|
|
210
|
+
const snapshotPath = path.join(workspace, `capture.${format}`);
|
|
211
|
+
const snapshotHandle = await open(snapshotPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
212
|
+
const digest = createHash('sha256');
|
|
213
|
+
let bytes = 0;
|
|
214
|
+
try {
|
|
215
|
+
const before = await sourceHandle.stat({ bigint: true });
|
|
216
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
217
|
+
while (bytes < expected.byteLength) {
|
|
218
|
+
const { bytesRead } = await sourceHandle.read(buffer, 0, Math.min(buffer.length, expected.byteLength - bytes), bytes);
|
|
219
|
+
if (bytesRead === 0)
|
|
220
|
+
break;
|
|
221
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
222
|
+
digest.update(chunk);
|
|
223
|
+
await snapshotHandle.write(chunk);
|
|
224
|
+
bytes += bytesRead;
|
|
225
|
+
}
|
|
226
|
+
await snapshotHandle.sync();
|
|
227
|
+
const after = await sourceHandle.stat({ bigint: true });
|
|
228
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs
|
|
229
|
+
|| bytes !== expected.byteLength || digest.digest('hex') !== expected.value) {
|
|
230
|
+
throw coded('CAPTURE_MUTATED', 'Capture changed while the isolated analysis snapshot was created.');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
await Promise.all([sourceHandle.close(), snapshotHandle.close()]);
|
|
235
|
+
}
|
|
236
|
+
await verifyEvidenceFile(source, expected);
|
|
237
|
+
const snapshotIdentity = await hashEvidenceFile(snapshotPath, 'derived-artifact');
|
|
238
|
+
if (snapshotIdentity.value !== expected.value || snapshotIdentity.byteLength !== expected.byteLength) {
|
|
239
|
+
throw coded('CAPTURE_SNAPSHOT_MISMATCH', 'The isolated analysis snapshot does not match the source identity.');
|
|
240
|
+
}
|
|
241
|
+
return { path: snapshotPath, format, identity: snapshotIdentity };
|
|
242
|
+
}
|
|
243
|
+
async function readPrefix(file) {
|
|
244
|
+
const handle = await open(file, constants.O_RDONLY | (typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0));
|
|
245
|
+
try {
|
|
246
|
+
const prefix = Buffer.alloc(32);
|
|
247
|
+
const { bytesRead } = await handle.read(prefix, 0, prefix.length, 0);
|
|
248
|
+
return prefix.subarray(0, bytesRead);
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
await handle.close();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function detectFormat(prefix) {
|
|
255
|
+
if (prefix.length >= 4 && prefix.subarray(0, 4).equals(Buffer.from([0x0a, 0x0d, 0x0d, 0x0a])))
|
|
256
|
+
return 'pcapng';
|
|
257
|
+
const magic = prefix.subarray(0, 4).toString('hex');
|
|
258
|
+
return ['d4c3b2a1', 'a1b2c3d4', '4d3cb2a1', 'a1b23c4d'].includes(magic) ? 'pcap' : null;
|
|
259
|
+
}
|
|
260
|
+
function buildTsharkArgs(capture, packetLimit, filterArgs, fields) {
|
|
261
|
+
const args = ['-n', '-r', capture, '-c', String(packetLimit), ...filterArgs, '-T', 'json', '--no-duplicate-keys'];
|
|
262
|
+
for (const field of fields)
|
|
263
|
+
args.push('-e', field);
|
|
264
|
+
return args;
|
|
265
|
+
}
|
|
266
|
+
function normalizeFields(requested) {
|
|
267
|
+
const values = requested ?? [...DEFAULT_METADATA_FIELDS];
|
|
268
|
+
if (values.length === 0 || values.length > 128)
|
|
269
|
+
throw coded('FIELD_SELECTION_INVALID', 'Select between 1 and 128 metadata fields.');
|
|
270
|
+
const unique = [...new Set(values)];
|
|
271
|
+
if (unique.length !== values.length)
|
|
272
|
+
throw coded('FIELD_SELECTION_INVALID', 'Field selection contains duplicates.');
|
|
273
|
+
for (const field of unique) {
|
|
274
|
+
if (!SAFE_FIELD.test(field) || BLOCKED_FIELD.test(field)) {
|
|
275
|
+
throw coded('FIELD_SELECTION_FORBIDDEN', `Field is not permitted in metadata-only output: ${field}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!unique.includes('frame.number'))
|
|
279
|
+
unique.unshift('frame.number');
|
|
280
|
+
return unique;
|
|
281
|
+
}
|
|
282
|
+
function parseTsharkJson(stdout, fields, packetLimit, allowTruncated = false) {
|
|
283
|
+
if (stdout.trim() === '')
|
|
284
|
+
return [];
|
|
285
|
+
let decoded;
|
|
286
|
+
try {
|
|
287
|
+
decoded = JSON.parse(stdout);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
if (!allowTruncated)
|
|
291
|
+
throw coded('TSHARK_OUTPUT_INVALID', 'TShark did not return valid bounded JSON output.');
|
|
292
|
+
decoded = recoverCompleteArrayObjects(stdout);
|
|
293
|
+
}
|
|
294
|
+
if (!Array.isArray(decoded))
|
|
295
|
+
throw coded('TSHARK_OUTPUT_INVALID', 'TShark JSON output must be an array.');
|
|
296
|
+
if (decoded.length > packetLimit)
|
|
297
|
+
throw coded('TSHARK_PACKET_LIMIT_BREACH', 'TShark returned more packets than the configured bound.');
|
|
298
|
+
return decoded.map((packet, index) => normalizePacket(packet, fields, index));
|
|
299
|
+
}
|
|
300
|
+
function recoverCompleteArrayObjects(value) {
|
|
301
|
+
const start = value.indexOf('[');
|
|
302
|
+
if (start < 0)
|
|
303
|
+
return [];
|
|
304
|
+
const recovered = [];
|
|
305
|
+
let objectStart = -1;
|
|
306
|
+
let depth = 0;
|
|
307
|
+
let quoted = false;
|
|
308
|
+
let escaped = false;
|
|
309
|
+
for (let index = start + 1; index < value.length; index += 1) {
|
|
310
|
+
const character = value[index];
|
|
311
|
+
if (quoted) {
|
|
312
|
+
if (escaped)
|
|
313
|
+
escaped = false;
|
|
314
|
+
else if (character === '\\')
|
|
315
|
+
escaped = true;
|
|
316
|
+
else if (character === '"')
|
|
317
|
+
quoted = false;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (character === '"') {
|
|
321
|
+
quoted = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (character === '{') {
|
|
325
|
+
if (depth === 0)
|
|
326
|
+
objectStart = index;
|
|
327
|
+
depth += 1;
|
|
328
|
+
}
|
|
329
|
+
else if (character === '}' && depth > 0) {
|
|
330
|
+
depth -= 1;
|
|
331
|
+
if (depth === 0 && objectStart >= 0) {
|
|
332
|
+
try {
|
|
333
|
+
recovered.push(JSON.parse(value.slice(objectStart, index + 1)));
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return recovered;
|
|
337
|
+
}
|
|
338
|
+
objectStart = -1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return recovered;
|
|
343
|
+
}
|
|
344
|
+
function normalizePacket(packet, fields, index) {
|
|
345
|
+
const layers = isRecord(packet) && isRecord(packet._source) && isRecord(packet._source.layers)
|
|
346
|
+
? packet._source.layers
|
|
347
|
+
: isRecord(packet) && isRecord(packet.layers) ? packet.layers : null;
|
|
348
|
+
if (!layers)
|
|
349
|
+
throw coded('TSHARK_OUTPUT_INVALID', `Packet ${index + 1} has no layers object.`);
|
|
350
|
+
const selected = {};
|
|
351
|
+
for (const field of fields) {
|
|
352
|
+
const value = scalar(layers[field]);
|
|
353
|
+
if (value !== undefined)
|
|
354
|
+
selected[field] = value;
|
|
355
|
+
}
|
|
356
|
+
const frame = integerField(selected['frame.number']);
|
|
357
|
+
if (frame < 1)
|
|
358
|
+
throw coded('TSHARK_OUTPUT_INVALID', `Packet ${index + 1} has no valid frame.number.`);
|
|
359
|
+
const time = numberField(selected['frame.time_epoch']);
|
|
360
|
+
return { frame, timeEpoch: time, fields: selected };
|
|
361
|
+
}
|
|
362
|
+
function buildBundle(input) {
|
|
363
|
+
const sourceDigest = `sha256:${input.sourceIdentity.value}`;
|
|
364
|
+
const records = input.packets.map(packet => ({ frame: packet.frame, fields: packet.fields }));
|
|
365
|
+
const derivedBytes = canonicalJson(records);
|
|
366
|
+
const derivedDigest = sha256(derivedBytes);
|
|
367
|
+
const artifactId = `artifact:packet-fields-${derivedDigest.slice(0, 16)}`;
|
|
368
|
+
const times = input.packets.map(packet => packet.timeEpoch).filter((value) => value !== null);
|
|
369
|
+
const frameEvidence = input.packets.map(packet => ({
|
|
370
|
+
evidence_id: `evidence:frame-${input.sourceIdentity.value.slice(0, 12)}-${packet.frame}`,
|
|
371
|
+
basis: 'observation',
|
|
372
|
+
statement: observationStatement(packet),
|
|
373
|
+
confidence: 'observed',
|
|
374
|
+
citations: packetCitations(packet, sourceDigest, input.context.context_digest),
|
|
375
|
+
observed_fields: Object.entries(packet.fields).map(([name, value]) => ({ name, value })),
|
|
376
|
+
}));
|
|
377
|
+
const evidence = input.packets.length > 0
|
|
378
|
+
? [captureSummaryEvidence(input.packets, sourceDigest, input.context.context_digest, input.sourceIdentity.value), ...frameEvidence]
|
|
379
|
+
: [];
|
|
380
|
+
const completedArtifact = input.status === 'completed' ? [{
|
|
381
|
+
artifact_id: artifactId,
|
|
382
|
+
class: 'derived',
|
|
383
|
+
media_type: 'application/json',
|
|
384
|
+
uri: `urn:sha256:${derivedDigest}`,
|
|
385
|
+
digest: { algorithm: 'sha256', value: derivedDigest },
|
|
386
|
+
data_handling: derivedDataHandling(),
|
|
387
|
+
}] : [];
|
|
388
|
+
return {
|
|
389
|
+
schema_version: PACKET_EVIDENCE_SCHEMA_VERSION,
|
|
390
|
+
kind: 'PacketEvidenceBundle',
|
|
391
|
+
bundle_id: `packet-evidence:${input.sourceIdentity.value.slice(0, 16)}-${input.context.context_digest.slice(7, 19)}`,
|
|
392
|
+
status: input.status,
|
|
393
|
+
created_at: input.started,
|
|
394
|
+
capture: {
|
|
395
|
+
capture_id: `capture:${input.sourceIdentity.value.slice(0, 24)}`,
|
|
396
|
+
capture_digest: sourceDigest,
|
|
397
|
+
source_uri: `file:${path.basename(input.options.capturePath)}`,
|
|
398
|
+
format: input.snapshot.format,
|
|
399
|
+
observed_time: {
|
|
400
|
+
start: times.length > 0 ? new Date(Math.min(...times) * 1000).toISOString() : null,
|
|
401
|
+
end: times.length > 0 ? new Date(Math.max(...times) * 1000).toISOString() : null,
|
|
402
|
+
},
|
|
403
|
+
hashes: {
|
|
404
|
+
source: { algorithm: 'sha256', value: input.sourceIdentity.value },
|
|
405
|
+
...(completedArtifact.length > 0 ? { derived: [{
|
|
406
|
+
artifact_id: artifactId,
|
|
407
|
+
digest: { algorithm: 'sha256', value: derivedDigest },
|
|
408
|
+
derivation: 'tshark-json-metadata-normalization',
|
|
409
|
+
}] } : {}),
|
|
410
|
+
},
|
|
411
|
+
tool_versions: [{ name: 'tshark', version: input.options.tshark.version, executable_path: input.options.tshark.path, capabilities: ['saved-capture-read', 'display-filter', 'json-fields'] }],
|
|
412
|
+
analysis_contexts: [input.context],
|
|
413
|
+
config_versions: [{
|
|
414
|
+
name: input.options.recipe.id,
|
|
415
|
+
version: input.options.recipe.version,
|
|
416
|
+
digest: { algorithm: 'sha256', value: sha256(canonicalJson(input.options.recipe)) },
|
|
417
|
+
}],
|
|
418
|
+
data_handling: sourceDataHandling(),
|
|
419
|
+
},
|
|
420
|
+
artifacts: completedArtifact,
|
|
421
|
+
evidence_items: evidence,
|
|
422
|
+
handoffs: [],
|
|
423
|
+
provenance: {
|
|
424
|
+
actor: input.options.actor ?? 'agent:network-analysis-offline',
|
|
425
|
+
activity: 'offline-pcap-inspection',
|
|
426
|
+
started_at: input.started,
|
|
427
|
+
ended_at: input.ended,
|
|
428
|
+
recipe_id: `analysis-recipe:${input.options.recipe.id}-${input.options.recipe.version}`,
|
|
429
|
+
authorization_refs: [...input.options.authorizationRefs],
|
|
430
|
+
},
|
|
431
|
+
errors: input.errors,
|
|
432
|
+
compatibility: {
|
|
433
|
+
contract: PACKET_EVIDENCE_SCHEMA_VERSION,
|
|
434
|
+
compatible_with: ['network-analysis.analysis-recipe/v1'],
|
|
435
|
+
unsupported_major_policy: 'fail-closed',
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function packetCitations(packet, sourceDigest, contextDigest) {
|
|
440
|
+
const citations = [
|
|
441
|
+
{ capture_digest: sourceDigest, locator: { type: 'frame', frame_number: packet.frame } },
|
|
442
|
+
];
|
|
443
|
+
const tcpStream = integerField(packet.fields['tcp.stream']);
|
|
444
|
+
const udpStream = integerField(packet.fields['udp.stream']);
|
|
445
|
+
if (tcpStream >= 0)
|
|
446
|
+
citations.push({
|
|
447
|
+
capture_digest: sourceDigest,
|
|
448
|
+
locator: { type: 'stream', protocol: 'tcp', stream_id: tcpStream, context_digest: contextDigest },
|
|
449
|
+
});
|
|
450
|
+
else if (udpStream >= 0)
|
|
451
|
+
citations.push({
|
|
452
|
+
capture_digest: sourceDigest,
|
|
453
|
+
locator: { type: 'stream', protocol: 'udp', stream_id: udpStream, context_digest: contextDigest },
|
|
454
|
+
});
|
|
455
|
+
return citations;
|
|
456
|
+
}
|
|
457
|
+
function captureSummaryEvidence(packets, sourceDigest, contextDigest, sourceHash) {
|
|
458
|
+
const protocols = new Map();
|
|
459
|
+
const endpoints = new Set();
|
|
460
|
+
const conversations = new Set();
|
|
461
|
+
let retransmissions = 0;
|
|
462
|
+
let resets = 0;
|
|
463
|
+
let encryptedFrames = 0;
|
|
464
|
+
for (const packet of packets) {
|
|
465
|
+
for (const protocol of packetProtocols(packet))
|
|
466
|
+
protocols.set(protocol, (protocols.get(protocol) ?? 0) + 1);
|
|
467
|
+
for (const field of ['eth.src', 'eth.dst', 'ip.src', 'ip.dst', 'ipv6.src', 'ipv6.dst']) {
|
|
468
|
+
const value = packet.fields[field];
|
|
469
|
+
if (typeof value === 'string' && value.length > 0)
|
|
470
|
+
endpoints.add(`${field.split('.')[0]}:${value}`);
|
|
471
|
+
}
|
|
472
|
+
const conversation = conversationIdentity(packet);
|
|
473
|
+
if (conversation)
|
|
474
|
+
conversations.add(conversation);
|
|
475
|
+
if (packet.fields['tcp.analysis.retransmission'] !== undefined)
|
|
476
|
+
retransmissions += 1;
|
|
477
|
+
if (truthyPacketField(packet.fields['tcp.flags.reset']))
|
|
478
|
+
resets += 1;
|
|
479
|
+
if (packet.fields['tls.handshake.type'] !== undefined)
|
|
480
|
+
encryptedFrames += 1;
|
|
481
|
+
}
|
|
482
|
+
const times = packets.map(packet => packet.timeEpoch).filter((value) => value !== null);
|
|
483
|
+
const duration = times.length > 0 ? Math.max(...times) - Math.min(...times) : 0;
|
|
484
|
+
const hierarchy = [...protocols.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
485
|
+
.map(([protocol, count]) => `${protocol}:${count}`).join(',');
|
|
486
|
+
const citedPackets = packets.length === 1 ? packets : [packets[0], packets[packets.length - 1]];
|
|
487
|
+
return {
|
|
488
|
+
evidence_id: `evidence:capture-summary-${sourceHash.slice(0, 16)}`,
|
|
489
|
+
basis: 'observation',
|
|
490
|
+
statement: `Observed ${packets.length} selected frame(s), ${endpoints.size} endpoint(s), and ${conversations.size} transport conversation(s).`,
|
|
491
|
+
confidence: 'observed',
|
|
492
|
+
citations: citedPackets.flatMap(packet => packetCitations(packet, sourceDigest, contextDigest).slice(0, 1)),
|
|
493
|
+
observed_fields: [
|
|
494
|
+
{ name: 'summary.packet_count', value: packets.length },
|
|
495
|
+
{ name: 'summary.protocol_hierarchy', value: hierarchy },
|
|
496
|
+
{ name: 'summary.endpoint_count', value: endpoints.size },
|
|
497
|
+
{ name: 'summary.conversation_count', value: conversations.size },
|
|
498
|
+
{ name: 'summary.duration_seconds', value: duration },
|
|
499
|
+
{ name: 'summary.tcp_retransmission_count', value: retransmissions },
|
|
500
|
+
{ name: 'summary.tcp_reset_count', value: resets },
|
|
501
|
+
{ name: 'summary.encrypted_transport_frame_count', value: encryptedFrames },
|
|
502
|
+
],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function packetProtocols(packet) {
|
|
506
|
+
return [
|
|
507
|
+
packet.fields['eth.src'] !== undefined ? 'Ethernet' : '',
|
|
508
|
+
packet.fields['ip.src'] !== undefined ? 'IPv4' : '',
|
|
509
|
+
packet.fields['ipv6.src'] !== undefined ? 'IPv6' : '',
|
|
510
|
+
packet.fields['tcp.stream'] !== undefined ? 'TCP' : '',
|
|
511
|
+
packet.fields['udp.stream'] !== undefined ? 'UDP' : '',
|
|
512
|
+
packet.fields['dns.qry.name'] !== undefined || packet.fields['dns.flags.response'] !== undefined ? 'DNS' : '',
|
|
513
|
+
packet.fields['tls.handshake.type'] !== undefined ? 'TLS' : '',
|
|
514
|
+
packet.fields['http.request.method'] !== undefined || packet.fields['http.response.code'] !== undefined ? 'HTTP' : '',
|
|
515
|
+
].filter(Boolean);
|
|
516
|
+
}
|
|
517
|
+
function conversationIdentity(packet) {
|
|
518
|
+
const protocol = packet.fields['tcp.stream'] !== undefined ? 'tcp' : packet.fields['udp.stream'] !== undefined ? 'udp' : null;
|
|
519
|
+
if (!protocol)
|
|
520
|
+
return null;
|
|
521
|
+
const leftAddress = String(packet.fields['ip.src'] ?? packet.fields['ipv6.src'] ?? '');
|
|
522
|
+
const rightAddress = String(packet.fields['ip.dst'] ?? packet.fields['ipv6.dst'] ?? '');
|
|
523
|
+
const leftPort = String(packet.fields[`${protocol}.srcport`] ?? '');
|
|
524
|
+
const rightPort = String(packet.fields[`${protocol}.dstport`] ?? '');
|
|
525
|
+
const peers = [`${leftAddress}:${leftPort}`, `${rightAddress}:${rightPort}`].sort();
|
|
526
|
+
return `${protocol}:${peers[0]}<->${peers[1]}`;
|
|
527
|
+
}
|
|
528
|
+
function truthyPacketField(value) {
|
|
529
|
+
return value === true || value === 1 || value === '1' || value === 'true' || value === 'True';
|
|
530
|
+
}
|
|
531
|
+
function analysisContext(options, commandArgv, fields) {
|
|
532
|
+
const emptyCaptureFilter = sha256('');
|
|
533
|
+
const displayFilter = options.recipe.displayFilter ?? '';
|
|
534
|
+
const configDigest = sha256(canonicalJson({ recipe: options.recipe, fields }));
|
|
535
|
+
const descriptor = {
|
|
536
|
+
tool: { name: 'tshark', version: options.tshark.version, executable_path: options.tshark.path },
|
|
537
|
+
profile: 'aiwg-isolated-empty',
|
|
538
|
+
command_argv: [path.basename(options.tshark.path), ...commandArgv],
|
|
539
|
+
config_digests: [`sha256:${configDigest}`],
|
|
540
|
+
capture_filter_digest: `sha256:${emptyCaptureFilter}`,
|
|
541
|
+
display_filter_digest: `sha256:${sha256(displayFilter)}`,
|
|
542
|
+
};
|
|
543
|
+
return { context_digest: `sha256:${sha256(canonicalJson(descriptor))}`, ...descriptor };
|
|
544
|
+
}
|
|
545
|
+
function classifyProcessResult(result, parsedCount) {
|
|
546
|
+
if (result.cancelled)
|
|
547
|
+
return [{ code: 'ANALYSIS_CANCELLED', message: `Analysis was cancelled; ${parsedCount} parsed packet record(s) were preserved.`, severity: 'warning' }];
|
|
548
|
+
if (result.timedOut)
|
|
549
|
+
return [{ code: 'ANALYSIS_TIMEOUT', message: `Analysis exceeded its runtime limit; ${parsedCount} parsed packet record(s) were preserved.`, severity: 'error' }];
|
|
550
|
+
if (result.outputLimited)
|
|
551
|
+
return [{ code: 'ANALYSIS_OUTPUT_LIMIT', message: `Analysis exceeded its output limit; ${parsedCount} parsed packet record(s) were preserved.`, severity: 'error' }];
|
|
552
|
+
if (result.exitCode !== 0)
|
|
553
|
+
return [{ code: 'TSHARK_FAILED', message: sanitizeDiagnostic(result.stderr) || 'TShark exited unsuccessfully.', severity: 'error' }];
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
function validateOptions(options) {
|
|
557
|
+
if (!path.isAbsolute(options.tshark.path) || !options.tshark.version.trim())
|
|
558
|
+
throw coded('TOOL_INVALID', 'TShark requires an absolute trusted path and version.');
|
|
559
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(options.recipe.id) || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(options.recipe.version)) {
|
|
560
|
+
throw coded('RECIPE_INVALID', 'Recipe requires a safe identifier and semantic version.');
|
|
561
|
+
}
|
|
562
|
+
if (options.authorizationRefs.length === 0 || options.authorizationRefs.some(value => !value.trim() || value.includes('\0'))) {
|
|
563
|
+
throw coded('AUTHORIZATION_REQUIRED', 'At least one capture authorization reference is required.');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function normalizeLimits(partial) {
|
|
567
|
+
const limits = {
|
|
568
|
+
inputBytes: partial?.inputBytes ?? 256 * 1024 * 1024,
|
|
569
|
+
packets: partial?.packets ?? 100_000,
|
|
570
|
+
outputBytes: partial?.outputBytes ?? 32 * 1024 * 1024,
|
|
571
|
+
timeoutMs: partial?.timeoutMs ?? 5 * 60 * 1000,
|
|
572
|
+
};
|
|
573
|
+
boundedInteger(limits.inputBytes, HARD_INPUT_LIMIT, 'Input byte limit');
|
|
574
|
+
boundedInteger(limits.packets, HARD_PACKET_LIMIT, 'Packet limit');
|
|
575
|
+
boundedInteger(limits.outputBytes, HARD_OUTPUT_LIMIT, 'Output byte limit');
|
|
576
|
+
boundedInteger(limits.timeoutMs, HARD_TIMEOUT_MS, 'Runtime limit');
|
|
577
|
+
return limits;
|
|
578
|
+
}
|
|
579
|
+
function boundedInteger(value, maximum, label) {
|
|
580
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
581
|
+
throw coded('LIMIT_INVALID', `${label} must be between 1 and ${maximum}.`);
|
|
582
|
+
}
|
|
583
|
+
function sourceDataHandling() {
|
|
584
|
+
return {
|
|
585
|
+
sensitivity: 'restricted',
|
|
586
|
+
payload_content: 'withheld',
|
|
587
|
+
redaction: { state: 'withheld', method: 'metadata-only-field-allowlist' },
|
|
588
|
+
retention: { class: 'source-controlled-by-owner' },
|
|
589
|
+
disclosure: { state: 'withheld', allowed_audiences: [] },
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function derivedDataHandling() {
|
|
593
|
+
return {
|
|
594
|
+
sensitivity: 'internal',
|
|
595
|
+
payload_content: 'metadata-only',
|
|
596
|
+
redaction: {
|
|
597
|
+
state: 'redacted',
|
|
598
|
+
method: 'metadata-only-field-allowlist',
|
|
599
|
+
redacted_fields: ['raw-packets', 'payload', 'credentials', 'cookies', 'authorization', 'tls-secrets', 'extracted-files'],
|
|
600
|
+
},
|
|
601
|
+
retention: { class: 'case-work-product' },
|
|
602
|
+
disclosure: { state: 'withheld', allowed_audiences: [] },
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function observationStatement(packet) {
|
|
606
|
+
const protocols = [
|
|
607
|
+
packet.fields['dns.qry.name'] !== undefined ? 'DNS' : '',
|
|
608
|
+
packet.fields['tls.handshake.type'] !== undefined ? 'TLS' : '',
|
|
609
|
+
packet.fields['http.request.method'] !== undefined || packet.fields['http.response.code'] !== undefined ? 'HTTP' : '',
|
|
610
|
+
packet.fields['tcp.stream'] !== undefined ? 'TCP' : '',
|
|
611
|
+
packet.fields['udp.stream'] !== undefined ? 'UDP' : '',
|
|
612
|
+
packet.fields['ipv6.src'] !== undefined ? 'IPv6' : packet.fields['ip.src'] !== undefined ? 'IPv4' : '',
|
|
613
|
+
].filter(Boolean);
|
|
614
|
+
return `Frame ${packet.frame} contains ${protocols.length > 0 ? protocols.join('/') : 'selected packet'} metadata.`;
|
|
615
|
+
}
|
|
616
|
+
function scalar(value) {
|
|
617
|
+
const candidate = Array.isArray(value) ? value[0] : value;
|
|
618
|
+
if (candidate === null || typeof candidate === 'string' || typeof candidate === 'number' || typeof candidate === 'boolean')
|
|
619
|
+
return candidate;
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
function integerField(value) {
|
|
623
|
+
const number = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;
|
|
624
|
+
return Number.isSafeInteger(number) ? number : -1;
|
|
625
|
+
}
|
|
626
|
+
function numberField(value) {
|
|
627
|
+
const number = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;
|
|
628
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
629
|
+
}
|
|
630
|
+
function isRecord(value) {
|
|
631
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
632
|
+
}
|
|
633
|
+
function sha256(value) {
|
|
634
|
+
return createHash('sha256').update(value).digest('hex');
|
|
635
|
+
}
|
|
636
|
+
function canonicalJson(value, space) {
|
|
637
|
+
return JSON.stringify(sortJson(value), null, space);
|
|
638
|
+
}
|
|
639
|
+
function sortJson(value) {
|
|
640
|
+
if (Array.isArray(value))
|
|
641
|
+
return value.map(sortJson);
|
|
642
|
+
if (!isRecord(value))
|
|
643
|
+
return value;
|
|
644
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, sortJson(value[key])]));
|
|
645
|
+
}
|
|
646
|
+
function isolatedAnalyzerEnv(configRoot) {
|
|
647
|
+
return {
|
|
648
|
+
PATH: '',
|
|
649
|
+
HOME: configRoot,
|
|
650
|
+
WIRESHARK_CONFIG_DIR: configRoot,
|
|
651
|
+
XDG_CONFIG_HOME: configRoot,
|
|
652
|
+
LC_ALL: 'C',
|
|
653
|
+
LANG: 'C',
|
|
654
|
+
TZ: 'UTC',
|
|
655
|
+
NO_COLOR: '1',
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function sanitizeDiagnostic(value) {
|
|
659
|
+
return value.replace(/[\r\n\t]+/g, ' ').replace(/(authorization|cookie|token|password|secret)\s*[:=]\s*\S+/gi, '$1=[REDACTED]').slice(0, 2048).trim();
|
|
660
|
+
}
|
|
661
|
+
function coded(code, message) {
|
|
662
|
+
return Object.assign(new Error(message), { code });
|
|
663
|
+
}
|
|
664
|
+
function errorCode(error) {
|
|
665
|
+
return typeof error?.code === 'string' && /^[A-Z][A-Z0-9_]+$/.test(error.code) ? error.code : 'CAPTURE_ANALYSIS_FAILED';
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=analyzer.js.map
|