@aiwg/cli 2026.9.4 → 2026.9.5
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 +1 -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,107 @@
|
|
|
1
|
+
/** Cross-reference checks after schema validation; file-byte integrity is the reader's responsibility. */
|
|
2
|
+
export function validateEvidenceReferences(bundle) {
|
|
3
|
+
const sourceDigest = `sha256:${digest(bundle.capture.hashes.source.value)}`;
|
|
4
|
+
if (bundle.capture.capture_digest !== sourceDigest)
|
|
5
|
+
throw new Error('Capture identity and source hash disagree');
|
|
6
|
+
const artifacts = new Map();
|
|
7
|
+
for (const artifact of bundle.artifacts) {
|
|
8
|
+
if (artifacts.has(artifact.artifact_id))
|
|
9
|
+
throw new Error('Duplicate artifact identity');
|
|
10
|
+
artifacts.set(artifact.artifact_id, digest(artifact.digest.value));
|
|
11
|
+
}
|
|
12
|
+
const derived = new Set();
|
|
13
|
+
for (const reference of bundle.capture.hashes.derived ?? []) {
|
|
14
|
+
if (derived.has(reference.artifact_id))
|
|
15
|
+
throw new Error('Duplicate derived artifact reference');
|
|
16
|
+
derived.add(reference.artifact_id);
|
|
17
|
+
if (artifacts.get(reference.artifact_id) !== digest(reference.digest.value))
|
|
18
|
+
throw new Error('Derived artifact identity or digest disagrees');
|
|
19
|
+
}
|
|
20
|
+
const contexts = new Set(bundle.capture.analysis_contexts.map(context => context.context_digest));
|
|
21
|
+
if (contexts.size !== bundle.capture.analysis_contexts.length)
|
|
22
|
+
throw new Error('Duplicate analysis context identity');
|
|
23
|
+
const evidenceIds = new Set(bundle.evidence_items.map(item => item.evidence_id));
|
|
24
|
+
if (evidenceIds.size !== bundle.evidence_items.length)
|
|
25
|
+
throw new Error('Duplicate evidence identity');
|
|
26
|
+
for (const item of bundle.evidence_items) {
|
|
27
|
+
for (const input of item.inference?.inputs ?? []) {
|
|
28
|
+
if (input === item.evidence_id || !evidenceIds.has(input))
|
|
29
|
+
throw new Error('Inference references missing or self evidence');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const item of [...bundle.evidence_items, ...bundle.handoffs]) {
|
|
33
|
+
for (const citation of item.citations) {
|
|
34
|
+
formatEvidenceCitation(citation);
|
|
35
|
+
if (citation.capture_digest !== sourceDigest)
|
|
36
|
+
throw new Error('Citation references a different capture');
|
|
37
|
+
if (citation.locator.type === 'stream' && !contexts.has(citation.locator.context_digest)) {
|
|
38
|
+
throw new Error('Citation references an unknown analysis context');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
44
|
+
function digest(value) {
|
|
45
|
+
if (typeof value !== 'string' || value.length !== 64 || !SHA256.test(value))
|
|
46
|
+
throw new Error('Citation requires a lowercase SHA-256 digest');
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function integer(value, minimum) {
|
|
50
|
+
if (!Number.isSafeInteger(value) || value < minimum)
|
|
51
|
+
throw new Error('Invalid packet citation locator');
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/** Convert the versioned evidence schema's locator into its portable citation. */
|
|
55
|
+
export function formatEvidenceCitation(citation) {
|
|
56
|
+
const hash = (value) => {
|
|
57
|
+
if (!value.startsWith('sha256:'))
|
|
58
|
+
throw new Error('Evidence citation requires SHA-256 identity');
|
|
59
|
+
return digest(value.slice(7));
|
|
60
|
+
};
|
|
61
|
+
const captureSha256 = hash(citation.capture_digest);
|
|
62
|
+
const locator = citation.locator;
|
|
63
|
+
if (locator.type === 'frame') {
|
|
64
|
+
return formatPacketCitation({ captureSha256, kind: 'frame', frameNumber: locator.frame_number });
|
|
65
|
+
}
|
|
66
|
+
if (locator.type !== 'stream')
|
|
67
|
+
throw new Error('Unsupported evidence locator');
|
|
68
|
+
return formatPacketCitation({ captureSha256, kind: 'stream', protocol: locator.protocol, streamId: locator.stream_id, contextSha256: hash(locator.context_digest) });
|
|
69
|
+
}
|
|
70
|
+
export function formatPacketCitation(citation) {
|
|
71
|
+
const base = `pcap:sha256:${digest(citation.captureSha256)}`;
|
|
72
|
+
if (citation.kind === 'frame')
|
|
73
|
+
return `${base}#frame=${integer(citation.frameNumber, 1)}`;
|
|
74
|
+
if (citation.kind !== 'stream' || !['tcp', 'udp'].includes(citation.protocol))
|
|
75
|
+
throw new Error('Unsupported packet citation kind');
|
|
76
|
+
return `${base}#stream=${citation.protocol}:${integer(citation.streamId, 0)}&context=${digest(citation.contextSha256)}`;
|
|
77
|
+
}
|
|
78
|
+
export function parsePacketCitation(value) {
|
|
79
|
+
if (typeof value !== 'string' || /\s/.test(value))
|
|
80
|
+
throw new Error('Invalid whitespace in packet citation');
|
|
81
|
+
const frame = /^pcap:sha256:([a-f0-9]{64})#frame=([1-9][0-9]*)$/.exec(value);
|
|
82
|
+
if (frame)
|
|
83
|
+
return { captureSha256: frame[1], kind: 'frame', frameNumber: integer(Number(frame[2]), 1) };
|
|
84
|
+
const stream = /^pcap:sha256:([a-f0-9]{64})#stream=(tcp|udp):(0|[1-9][0-9]*)&context=([a-f0-9]{64})$/.exec(value);
|
|
85
|
+
if (stream)
|
|
86
|
+
return { captureSha256: stream[1], kind: 'stream', protocol: stream[2], streamId: integer(Number(stream[3]), 0), contextSha256: stream[4] };
|
|
87
|
+
throw new Error('Invalid packet citation; use capture digest and frame or context-bound stream identity');
|
|
88
|
+
}
|
|
89
|
+
/** Verify identity before selecting a locator; a citation is never a filesystem path. */
|
|
90
|
+
export function resolvePacketCitation(value, capture) {
|
|
91
|
+
const citation = parsePacketCitation(value);
|
|
92
|
+
if (citation.captureSha256 !== digest(capture.sha256))
|
|
93
|
+
throw new Error('Capture digest does not match citation');
|
|
94
|
+
if (citation.kind === 'frame') {
|
|
95
|
+
if (citation.frameNumber > integer(capture.frameCount, 0))
|
|
96
|
+
throw new Error('Cited frame is absent from capture');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
if (citation.contextSha256 !== capture.contextSha256)
|
|
100
|
+
throw new Error('Stream analysis context does not match citation');
|
|
101
|
+
if (!capture.streams?.some(stream => stream.protocol === citation.protocol && stream.id === citation.streamId)) {
|
|
102
|
+
throw new Error('Cited stream is absent from analysis');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return citation;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=citations.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { formatEvidenceCitation, validateEvidenceReferences } from './citations.js';
|
|
2
|
+
import { assertLocalOutputAllowed } from './governance.js';
|
|
3
|
+
export const FORENSIC_PACKET_EVIDENCE_SCHEMA = 'aiwg.forensics.packet-evidence.v1';
|
|
4
|
+
export const ATTACK_NETWORK_TECHNIQUES = Object.freeze({
|
|
5
|
+
'T1071.001': { name: 'Application Layer Protocol: Web Protocols', url: 'https://attack.mitre.org/techniques/T1071/001/' },
|
|
6
|
+
'T1071.004': { name: 'Application Layer Protocol: DNS', url: 'https://attack.mitre.org/techniques/T1071/004/' },
|
|
7
|
+
T1573: { name: 'Encrypted Channel', url: 'https://attack.mitre.org/techniques/T1573/' },
|
|
8
|
+
});
|
|
9
|
+
export function createForensicPacketEvidenceEntry(input) {
|
|
10
|
+
validateEvidenceReferences(input.evidence);
|
|
11
|
+
const captureDigest = requiredString(input.evidence.capture.capture_digest, 'Capture digest');
|
|
12
|
+
if (input.verifiedCaptureDigest !== captureDigest)
|
|
13
|
+
throw new Error('Forensic handoff capture digest was not independently verified');
|
|
14
|
+
for (const [label, value] of Object.entries(input).filter(([key]) => key !== 'evidence' && key !== 'verifiedCaptureDigest')) {
|
|
15
|
+
requiredString(value, `Forensic ${label}`);
|
|
16
|
+
}
|
|
17
|
+
if (!/^E-[0-9]{3,}$/.test(input.evidenceId))
|
|
18
|
+
throw new Error('Forensic packet evidence requires an E-NNN evidence ID');
|
|
19
|
+
const sourceHash = requiredString(input.evidence.capture.hashes?.source?.value, 'Capture source hash');
|
|
20
|
+
const locatorMap = new Map();
|
|
21
|
+
for (const item of input.evidence.evidence_items) {
|
|
22
|
+
for (const citation of item.citations)
|
|
23
|
+
locatorMap.set(formatEvidenceCitation(citation), citation);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
schema: FORENSIC_PACKET_EVIDENCE_SCHEMA,
|
|
27
|
+
evidenceId: input.evidenceId,
|
|
28
|
+
evidenceType: 'packet-evidence-bundle',
|
|
29
|
+
caseId: input.caseId,
|
|
30
|
+
bundleId: input.evidence.bundle_id,
|
|
31
|
+
bundleStatus: input.evidence.status,
|
|
32
|
+
captureDigest,
|
|
33
|
+
sourceHash: { algorithm: 'sha256', value: sourceHash },
|
|
34
|
+
analysisContexts: (input.evidence.capture.analysis_contexts ?? []).map(context => ({
|
|
35
|
+
contextDigest: requiredString(context.context_digest, 'Analysis context digest'),
|
|
36
|
+
tool: {
|
|
37
|
+
name: requiredString(context.tool?.name, 'Analysis tool name'),
|
|
38
|
+
version: requiredString(context.tool?.version, 'Analysis tool version'),
|
|
39
|
+
executablePath: requiredString(context.tool?.executable_path, 'Analysis tool path'),
|
|
40
|
+
},
|
|
41
|
+
commandArgv: stringArray(context.command_argv, 'Analysis command argv'),
|
|
42
|
+
displayFilterDigest: requiredString(context.display_filter_digest, 'Display-filter digest'),
|
|
43
|
+
captureFilterDigest: requiredString(context.capture_filter_digest, 'Capture-filter digest'),
|
|
44
|
+
configDigests: stringArray(context.config_digests, 'Analysis config digests'),
|
|
45
|
+
})),
|
|
46
|
+
artifacts: input.evidence.artifacts.map((artifact) => ({
|
|
47
|
+
artifactId: requiredString(artifact.artifact_id, 'Artifact ID'),
|
|
48
|
+
mediaType: requiredString(artifact.media_type, 'Artifact media type'),
|
|
49
|
+
uri: requiredString(artifact.uri, 'Artifact URI'),
|
|
50
|
+
digest: `${requiredString(artifact.digest?.algorithm, 'Artifact digest algorithm')}:${requiredString(artifact.digest?.value, 'Artifact digest')}`,
|
|
51
|
+
})),
|
|
52
|
+
locators: [...locatorMap.entries()].map(([citation, value]) => ({ citation, locator: structuredClone(value.locator) })),
|
|
53
|
+
custody: {
|
|
54
|
+
receivedAt: input.receivedAt,
|
|
55
|
+
receivedFrom: input.receivedFrom,
|
|
56
|
+
receivedBy: input.receivedBy,
|
|
57
|
+
storageLocation: input.storageLocation,
|
|
58
|
+
authorizationRef: input.authorizationRef,
|
|
59
|
+
method: 'validated-bundle-handoff',
|
|
60
|
+
integrity: 'sha256-verified',
|
|
61
|
+
},
|
|
62
|
+
handling: { payload: 'withheld', disclosure: 'withheld', sensitivity: 'restricted' },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function appendPacketEvidenceToForensicManifest(manifest, entry) {
|
|
66
|
+
const existing = manifest.evidenceItems ?? [];
|
|
67
|
+
if (existing.some((item) => item?.evidenceId === entry.evidenceId))
|
|
68
|
+
throw new Error(`Duplicate forensic evidence ID ${entry.evidenceId}`);
|
|
69
|
+
return { ...structuredClone(manifest), evidenceItems: [...structuredClone(existing), structuredClone(entry)] };
|
|
70
|
+
}
|
|
71
|
+
export function createForensicNetworkFinding(evidence, input) {
|
|
72
|
+
if (!/^F-[0-9]{3,}$/.test(input.findingId))
|
|
73
|
+
throw new Error('Forensic finding requires an F-NNN identifier');
|
|
74
|
+
requiredString(input.statement, 'Finding statement');
|
|
75
|
+
requiredString(input.analyst, 'Finding analyst');
|
|
76
|
+
requiredString(input.recordedAt, 'Finding timestamp');
|
|
77
|
+
stringArray(input.falsePositiveNotes, 'False-positive notes');
|
|
78
|
+
if (input.falsePositiveNotes.length === 0)
|
|
79
|
+
throw new Error('Forensic finding requires false-positive notes');
|
|
80
|
+
const available = new Map(evidence.locators.map(locator => [locator.citation, locator]));
|
|
81
|
+
const citations = input.citations.map(citation => {
|
|
82
|
+
const formatted = formatEvidenceCitation(citation);
|
|
83
|
+
if (citation.capture_digest !== evidence.captureDigest || !available.has(formatted)) {
|
|
84
|
+
throw new Error('Forensic finding citation is absent from the evidence manifest');
|
|
85
|
+
}
|
|
86
|
+
return structuredClone(available.get(formatted));
|
|
87
|
+
});
|
|
88
|
+
if (citations.length === 0)
|
|
89
|
+
throw new Error('Forensic finding requires at least one stable packet citation');
|
|
90
|
+
const attack = (input.attackTechniques ?? []).map(techniqueId => {
|
|
91
|
+
const technique = ATTACK_NETWORK_TECHNIQUES[techniqueId];
|
|
92
|
+
if (!technique)
|
|
93
|
+
throw new Error(`Unsupported ATT&CK network technique ${techniqueId}`);
|
|
94
|
+
return { techniqueId, ...technique };
|
|
95
|
+
});
|
|
96
|
+
const contentSensitivity = input.contentSensitivity ?? 'metadata';
|
|
97
|
+
if (contentSensitivity === 'payload-sensitive') {
|
|
98
|
+
if (!input.payloadAuthorization?.approvalRef.trim())
|
|
99
|
+
throw new Error('Payload-sensitive finding requires an explicit approval reference');
|
|
100
|
+
assertLocalOutputAllowed(input.payloadAuthorization.policy, { content: ['metadata', 'payload'], payloadOptIn: input.payloadAuthorization.payloadOptIn });
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
findingId: input.findingId,
|
|
104
|
+
caseId: evidence.caseId,
|
|
105
|
+
evidenceId: evidence.evidenceId,
|
|
106
|
+
category: input.category,
|
|
107
|
+
severity: input.severity,
|
|
108
|
+
confidence: input.confidence,
|
|
109
|
+
kind: input.kind,
|
|
110
|
+
statement: input.statement,
|
|
111
|
+
analyst: input.analyst,
|
|
112
|
+
recordedAt: input.recordedAt,
|
|
113
|
+
falsePositiveNotes: [...input.falsePositiveNotes],
|
|
114
|
+
attack,
|
|
115
|
+
citations,
|
|
116
|
+
provenance: { captureDigest: evidence.captureDigest, contexts: structuredClone(evidence.analysisContexts) },
|
|
117
|
+
contentSensitivity,
|
|
118
|
+
...(input.payloadAuthorization ? { payloadApprovalRef: input.payloadAuthorization.approvalRef } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function requiredString(value, label) {
|
|
122
|
+
if (typeof value !== 'string' || !value.trim() || value.includes('\0'))
|
|
123
|
+
throw new Error(`${label} is required`);
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
function stringArray(value, label) {
|
|
127
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || !item.trim() || item.includes('\0'))) {
|
|
128
|
+
throw new Error(`${label} must contain non-empty strings`);
|
|
129
|
+
}
|
|
130
|
+
return [...value];
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=forensics.js.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants, lstat, open, realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const NETWORK_ANALYSIS_GOVERNANCE_SCHEMA = '1.0.0';
|
|
5
|
+
export const DEFAULT_NETWORK_ANALYSIS_POLICY = Object.freeze({
|
|
6
|
+
acquisition: 'offline-only',
|
|
7
|
+
output: 'metadata-only',
|
|
8
|
+
payloadAccess: 'deny',
|
|
9
|
+
providerTransfer: 'deny',
|
|
10
|
+
});
|
|
11
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
12
|
+
export function assertLiveCaptureAuthorized(policy, authorization, request, now = new Date()) {
|
|
13
|
+
if (policy.acquisition !== 'live-capture-explicit-authorization')
|
|
14
|
+
fail('Live capture is disabled by policy');
|
|
15
|
+
if (authorization.state !== 'authorized')
|
|
16
|
+
fail('Live capture authorization is not active');
|
|
17
|
+
if (!authorization.authorizationId || !authorization.authority.principal || !authorization.authority.basis || !authorization.authority.approvedBy) {
|
|
18
|
+
fail('Live capture requires a complete authority record');
|
|
19
|
+
}
|
|
20
|
+
assertActiveWindow(authorization.issuedAt, authorization.expiresAt, now, 'Live capture authorization');
|
|
21
|
+
assertCaptureScope(authorization);
|
|
22
|
+
assertCaptureScope(request);
|
|
23
|
+
if (request.interface !== authorization.interface)
|
|
24
|
+
fail('Capture interface exceeds authorization');
|
|
25
|
+
if (request.captureFilter.expression !== authorization.captureFilter.expression)
|
|
26
|
+
fail('Capture filter exceeds authorization');
|
|
27
|
+
if (request.destination.path !== authorization.destination.path || request.destination.overwrite !== false)
|
|
28
|
+
fail('Capture destination exceeds authorization');
|
|
29
|
+
if (request.retention.class !== authorization.retention.class || request.retention.deleteAt !== authorization.retention.deleteAt) {
|
|
30
|
+
fail('Capture retention differs from authorization');
|
|
31
|
+
}
|
|
32
|
+
if (timestamp(authorization.retention.deleteAt, 'Retention deletion time') <= now.getTime())
|
|
33
|
+
fail('Capture retention has already expired');
|
|
34
|
+
if (request.limits.durationSeconds > authorization.limits.durationSeconds
|
|
35
|
+
|| request.limits.byteLimit > authorization.limits.byteLimit
|
|
36
|
+
|| request.limits.fileCount > authorization.limits.fileCount) {
|
|
37
|
+
fail('Capture limits exceed authorization');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function assertLocalOutputAllowed(policy, request) {
|
|
41
|
+
const content = unique(request.content, 'Output content');
|
|
42
|
+
assertKnownContent(content);
|
|
43
|
+
if (!content.includes('metadata'))
|
|
44
|
+
fail('Output must be metadata-first');
|
|
45
|
+
if (content.includes('headers') && policy.output !== 'metadata-and-headers')
|
|
46
|
+
fail('Header output requires explicit policy');
|
|
47
|
+
const containsPayload = content.includes('payload') || content.includes('raw-packets');
|
|
48
|
+
if (containsPayload && (policy.payloadAccess !== 'explicit-opt-in' || request.payloadOptIn !== true)) {
|
|
49
|
+
fail('Payload and raw-packet output require explicit policy and request opt-in');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function assertProviderTransferAllowed(policy, decision, request, now = new Date()) {
|
|
53
|
+
if (policy.providerTransfer !== 'explicit-decision')
|
|
54
|
+
fail('Provider transfer is disabled by policy');
|
|
55
|
+
if (decision.state !== 'allow')
|
|
56
|
+
fail('Provider disclosure decision does not allow transfer');
|
|
57
|
+
if (!decision.decisionId || !decision.decidedBy || !decision.provider || !decision.purpose)
|
|
58
|
+
fail('Provider disclosure decision is incomplete');
|
|
59
|
+
assertActiveWindow(decision.issuedAt, decision.expiresAt, now, 'Provider disclosure decision');
|
|
60
|
+
if (!SHA256.test(request.captureDigest) || request.captureDigest !== decision.captureDigest)
|
|
61
|
+
fail('Provider decision does not match capture identity');
|
|
62
|
+
if (request.provider !== decision.provider || request.purpose !== decision.purpose)
|
|
63
|
+
fail('Provider or purpose exceeds disclosure decision');
|
|
64
|
+
const content = unique(request.content, 'Provider content');
|
|
65
|
+
const fields = unique(request.fields, 'Provider fields');
|
|
66
|
+
assertKnownContent(content);
|
|
67
|
+
unique(decision.allowedContent, 'Allowed provider content');
|
|
68
|
+
unique(decision.allowedFields, 'Allowed provider fields');
|
|
69
|
+
if (content.length === 0 || fields.length === 0)
|
|
70
|
+
fail('Provider content and fields must be declared');
|
|
71
|
+
if (content.some(item => !decision.allowedContent.includes(item)))
|
|
72
|
+
fail('Provider content exceeds disclosure decision');
|
|
73
|
+
if (fields.some(item => !decision.allowedFields.includes(item)))
|
|
74
|
+
fail('Provider fields exceed disclosure decision');
|
|
75
|
+
if ((content.includes('payload') || content.includes('raw-packets')) && decision.payloadExplicitlyAuthorized !== true) {
|
|
76
|
+
fail('Provider payload transfer requires explicit payload authorization');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Produce an execFile/spawn-compatible invocation. Filters remain individual argv entries. */
|
|
80
|
+
export function safeProcessSpec(file, args) {
|
|
81
|
+
if (!path.isAbsolute(file) || file.includes('\0'))
|
|
82
|
+
fail('Executable path must be absolute and contain no NUL byte');
|
|
83
|
+
const safeArgs = args.map(argument => {
|
|
84
|
+
if (typeof argument !== 'string' || argument.includes('\0'))
|
|
85
|
+
fail('Process arguments must be strings without NUL bytes');
|
|
86
|
+
return argument;
|
|
87
|
+
});
|
|
88
|
+
return Object.freeze({ file, args: Object.freeze(safeArgs), shell: false });
|
|
89
|
+
}
|
|
90
|
+
export function tsharkFilterArgs(filters) {
|
|
91
|
+
const args = [];
|
|
92
|
+
if (filters.captureFilter) {
|
|
93
|
+
assertText(filters.captureFilter.expression, 'Capture filter', 8192);
|
|
94
|
+
args.push('-f', filters.captureFilter.expression);
|
|
95
|
+
}
|
|
96
|
+
if (filters.displayFilter) {
|
|
97
|
+
assertText(filters.displayFilter.expression, 'Display filter', 8192);
|
|
98
|
+
args.push('-Y', filters.displayFilter.expression);
|
|
99
|
+
}
|
|
100
|
+
return args;
|
|
101
|
+
}
|
|
102
|
+
export async function resolveCaptureDestination(allowedRoot, destination) {
|
|
103
|
+
if (!path.isAbsolute(allowedRoot) || !path.isAbsolute(destination))
|
|
104
|
+
fail('Capture root and destination must be absolute');
|
|
105
|
+
const root = await realpath(allowedRoot);
|
|
106
|
+
const resolved = path.resolve(destination);
|
|
107
|
+
const parent = await realpath(path.dirname(resolved));
|
|
108
|
+
const relative = path.relative(root, parent);
|
|
109
|
+
if (relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) {
|
|
110
|
+
fail('Capture destination must be a file below the allowed root');
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
await lstat(resolved);
|
|
114
|
+
fail('Capture destination already exists');
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error?.code !== 'ENOENT')
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
return path.join(parent, path.basename(resolved));
|
|
121
|
+
}
|
|
122
|
+
/** Hashes one regular, non-symlink file without changing it and rejects concurrent mutation. */
|
|
123
|
+
export async function hashEvidenceFile(file, role) {
|
|
124
|
+
const link = await lstat(file);
|
|
125
|
+
if (link.isSymbolicLink() || !link.isFile())
|
|
126
|
+
fail('Evidence path must be a regular non-symlink file');
|
|
127
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
128
|
+
const handle = await open(file, constants.O_RDONLY | noFollow);
|
|
129
|
+
try {
|
|
130
|
+
const before = await handle.stat({ bigint: true });
|
|
131
|
+
if (!before.isFile())
|
|
132
|
+
fail('Evidence descriptor must reference a regular file');
|
|
133
|
+
if (before.dev !== BigInt(link.dev) || before.ino !== BigInt(link.ino))
|
|
134
|
+
fail('Evidence path changed before hashing');
|
|
135
|
+
if (before.size > BigInt(Number.MAX_SAFE_INTEGER))
|
|
136
|
+
fail('Evidence file is too large to represent safely');
|
|
137
|
+
const hash = createHash('sha256');
|
|
138
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
139
|
+
let position = 0;
|
|
140
|
+
while (position < before.size) {
|
|
141
|
+
const remaining = before.size - BigInt(position);
|
|
142
|
+
const length = Number(remaining < BigInt(buffer.length) ? remaining : BigInt(buffer.length));
|
|
143
|
+
const { bytesRead } = await handle.read(buffer, 0, length, position);
|
|
144
|
+
if (bytesRead === 0)
|
|
145
|
+
break;
|
|
146
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
147
|
+
position += bytesRead;
|
|
148
|
+
}
|
|
149
|
+
const after = await handle.stat({ bigint: true });
|
|
150
|
+
const linkAfter = await lstat(file);
|
|
151
|
+
if (position !== Number(before.size) || before.dev !== after.dev || before.ino !== after.ino
|
|
152
|
+
|| before.size !== after.size || before.mtimeNs !== after.mtimeNs || linkAfter.isSymbolicLink()
|
|
153
|
+
|| before.dev !== BigInt(linkAfter.dev) || before.ino !== BigInt(linkAfter.ino)) {
|
|
154
|
+
fail('Evidence file changed while it was being hashed');
|
|
155
|
+
}
|
|
156
|
+
return { role, algorithm: 'sha256', value: hash.digest('hex'), byteLength: Number(before.size) };
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
await handle.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export async function verifyEvidenceFile(file, expected) {
|
|
163
|
+
const actual = await hashEvidenceFile(file, expected.role);
|
|
164
|
+
if (actual.algorithm !== expected.algorithm || actual.value !== expected.value || actual.byteLength !== expected.byteLength) {
|
|
165
|
+
fail('Evidence file no longer matches its recorded identity');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function assertCaptureScope(scope) {
|
|
169
|
+
assertText(scope.interface, 'Capture interface', 255);
|
|
170
|
+
if (scope.captureFilter.type !== 'capture_filter' || scope.captureFilter.language !== 'bpf')
|
|
171
|
+
fail('Live capture requires a typed BPF capture filter');
|
|
172
|
+
assertText(scope.captureFilter.expression, 'Capture filter', 8192);
|
|
173
|
+
positiveInteger(scope.limits.durationSeconds, 86400, 'Capture duration');
|
|
174
|
+
positiveInteger(scope.limits.byteLimit, 10 * 1024 * 1024 * 1024, 'Capture byte limit');
|
|
175
|
+
positiveInteger(scope.limits.fileCount, 100, 'Capture file count');
|
|
176
|
+
if (!path.isAbsolute(scope.destination.path) || scope.destination.overwrite !== false)
|
|
177
|
+
fail('Capture destination must be absolute and non-overwriting');
|
|
178
|
+
assertText(scope.retention.class, 'Retention class', 64);
|
|
179
|
+
if (scope.retention.disposition !== 'verified-delete')
|
|
180
|
+
fail('Capture retention requires verified deletion');
|
|
181
|
+
timestamp(scope.retention.deleteAt, 'Retention deletion time');
|
|
182
|
+
}
|
|
183
|
+
function assertActiveWindow(issuedAt, expiresAt, now, label) {
|
|
184
|
+
const issued = timestamp(issuedAt, `${label} issued time`);
|
|
185
|
+
const expires = timestamp(expiresAt, `${label} expiry time`);
|
|
186
|
+
if (issued > expires || now.getTime() < issued || now.getTime() >= expires)
|
|
187
|
+
fail(`${label} is not active`);
|
|
188
|
+
}
|
|
189
|
+
function timestamp(value, label) {
|
|
190
|
+
const parsed = Date.parse(value);
|
|
191
|
+
if (!Number.isFinite(parsed))
|
|
192
|
+
fail(`${label} must be an ISO-8601 timestamp`);
|
|
193
|
+
return parsed;
|
|
194
|
+
}
|
|
195
|
+
function positiveInteger(value, maximum, label) {
|
|
196
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
197
|
+
fail(`${label} is outside its hard bound`);
|
|
198
|
+
}
|
|
199
|
+
function assertText(value, label, maximum) {
|
|
200
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > maximum || value.includes('\0'))
|
|
201
|
+
fail(`${label} is invalid`);
|
|
202
|
+
}
|
|
203
|
+
function unique(values, label) {
|
|
204
|
+
if (new Set(values).size !== values.length)
|
|
205
|
+
fail(`${label} contains duplicates`);
|
|
206
|
+
return values;
|
|
207
|
+
}
|
|
208
|
+
function assertKnownContent(values) {
|
|
209
|
+
const allowed = new Set(['metadata', 'headers', 'raw-packets', 'payload']);
|
|
210
|
+
if (values.some(value => !allowed.has(value)))
|
|
211
|
+
fail('Unknown governed content class');
|
|
212
|
+
}
|
|
213
|
+
function fail(message) {
|
|
214
|
+
throw new Error(message);
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=governance.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './analyzer.js';
|
|
2
|
+
export * from './citations.js';
|
|
3
|
+
export * from './forensics.js';
|
|
4
|
+
export * from './governance.js';
|
|
5
|
+
export * from './probe.js';
|
|
6
|
+
export * from './recipes.js';
|
|
7
|
+
export * from './research.js';
|
|
8
|
+
export * from './termshark.js';
|
|
9
|
+
export * from './verification.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|