@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1
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/CHANGELOG.md +110 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
- package/dist/cli.js +10009 -9654
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-discovery.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +73 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/internal-urls/security-protocol.d.ts +15 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +18 -0
- package/dist/types/security/auth.d.ts +30 -0
- package/dist/types/security/cloud.d.ts +79 -0
- package/dist/types/security/comparison.d.ts +49 -0
- package/dist/types/security/contracts/ids.d.ts +15 -0
- package/dist/types/security/contracts/index.d.ts +4 -0
- package/dist/types/security/contracts/schemas.d.ts +660 -0
- package/dist/types/security/contracts/types.d.ts +234 -0
- package/dist/types/security/contracts/validation.d.ts +5 -0
- package/dist/types/security/coordinator.d.ts +100 -0
- package/dist/types/security/importers/codex-security.d.ts +7 -0
- package/dist/types/security/importers/index.d.ts +2 -0
- package/dist/types/security/importers/sarif.d.ts +9 -0
- package/dist/types/security/index.d.ts +13 -0
- package/dist/types/security/preflight.d.ts +61 -0
- package/dist/types/security/provenance.d.ts +24 -0
- package/dist/types/security/publication.d.ts +78 -0
- package/dist/types/security/remediation.d.ts +27 -0
- package/dist/types/security/resource-output.d.ts +8 -0
- package/dist/types/security/sarif.d.ts +2 -0
- package/dist/types/security/store.d.ts +40 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/slash-commands/helpers/security.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/executor.d.ts +14 -2
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +10 -2
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/security-scan.d.ts +96 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/scripts/security-compare.ts +40 -0
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-discovery.ts +32 -5
- package/src/config/model-registry.ts +107 -25
- package/src/config/settings-schema.ts +77 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/eval/py/prelude.py +7 -3
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/internal-urls/router.ts +4 -1
- package/src/internal-urls/security-protocol.ts +261 -0
- package/src/internal-urls/types.ts +1 -1
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +7 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +15 -9
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +31 -17
- package/src/modes/rpc/host-uris.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/agents/security-reviewer.md +75 -0
- package/src/prompts/security/scan-coordinator.md +7 -0
- package/src/prompts/security/scan-request.md +21 -0
- package/src/prompts/security/validate-request.md +8 -0
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/grep.md +7 -6
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/sdk.ts +212 -21
- package/src/security/auth.ts +98 -0
- package/src/security/cloud.ts +686 -0
- package/src/security/comparison.ts +255 -0
- package/src/security/contracts/ids.ts +111 -0
- package/src/security/contracts/index.ts +4 -0
- package/src/security/contracts/schemas.ts +201 -0
- package/src/security/contracts/types.ts +254 -0
- package/src/security/contracts/validation.ts +65 -0
- package/src/security/coordinator.ts +708 -0
- package/src/security/importers/codex-security.ts +387 -0
- package/src/security/importers/index.ts +2 -0
- package/src/security/importers/sarif.ts +357 -0
- package/src/security/index.ts +13 -0
- package/src/security/preflight.ts +405 -0
- package/src/security/provenance.ts +106 -0
- package/src/security/publication.ts +326 -0
- package/src/security/remediation.ts +93 -0
- package/src/security/resource-output.ts +50 -0
- package/src/security/sarif.ts +78 -0
- package/src/security/store.ts +430 -0
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +302 -175
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +152 -3
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +20 -4
- package/src/task/render.ts +20 -4
- package/src/task/structured-subagent.ts +6 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +37 -5
- package/src/tools/path-utils.ts +91 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/security-scan.ts +287 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/types.ts +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { SecurityComparisonReport, SecurityFinding, SecurityFindingMatch, SecurityScanBundle } from "./contracts";
|
|
2
|
+
|
|
3
|
+
export interface SecurityDifferentialFindingMatch {
|
|
4
|
+
referenceFindingId: string;
|
|
5
|
+
candidateFindingId: string;
|
|
6
|
+
basis: "fingerprint" | "rule_location" | "taxonomy_location";
|
|
7
|
+
}
|
|
8
|
+
export interface SecurityDifferentialFindingSummary {
|
|
9
|
+
findingId: string;
|
|
10
|
+
ruleId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
severity: SecurityFinding["severity"]["level"];
|
|
13
|
+
confidence: SecurityFinding["confidence"]["level"];
|
|
14
|
+
validationStatus: SecurityFinding["validation"]["status"];
|
|
15
|
+
dispositionStatus: SecurityFinding["disposition"]["status"];
|
|
16
|
+
primaryLocation?: { path: string; startLine: number };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SecurityDifferentialScanSummary {
|
|
20
|
+
scanId: string;
|
|
21
|
+
producer: SecurityScanBundle["scan"]["producer"];
|
|
22
|
+
status: SecurityScanBundle["scan"]["status"];
|
|
23
|
+
findingCount: number;
|
|
24
|
+
actionableFindingCount: number;
|
|
25
|
+
validatedFindingCount: number;
|
|
26
|
+
rejectedFindingCount: number;
|
|
27
|
+
coverage: SecurityScanBundle["scan"]["coverage"];
|
|
28
|
+
metrics?: SecurityScanBundle["scan"]["metrics"];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SecurityDifferentialReport {
|
|
32
|
+
referenceScanId: string;
|
|
33
|
+
candidateScanId: string;
|
|
34
|
+
matches: SecurityDifferentialFindingMatch[];
|
|
35
|
+
referenceOnlyFindingIds: string[];
|
|
36
|
+
candidateOnlyFindingIds: string[];
|
|
37
|
+
reference: SecurityDifferentialScanSummary;
|
|
38
|
+
candidate: SecurityDifferentialScanSummary;
|
|
39
|
+
referenceOnlyFindings: SecurityDifferentialFindingSummary[];
|
|
40
|
+
candidateOnlyFindings: SecurityDifferentialFindingSummary[];
|
|
41
|
+
referenceFindingCount: number;
|
|
42
|
+
candidateFindingCount: number;
|
|
43
|
+
matchedFindingCount: number;
|
|
44
|
+
recallAgainstReference: number;
|
|
45
|
+
precisionAgainstReference: number;
|
|
46
|
+
jaccardOverlap: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizedPrimaryLocation(finding: SecurityFinding): string | undefined {
|
|
50
|
+
const location = finding.occurrences.flatMap(occurrence => occurrence.locations)[0];
|
|
51
|
+
if (!location) return undefined;
|
|
52
|
+
return `${location.path.replaceAll("\\", "/").replace(/^\.\//, "").toLowerCase()}:${location.startLine}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fallbackKey(finding: SecurityFinding): string | undefined {
|
|
56
|
+
const location = normalizedPrimaryLocation(finding);
|
|
57
|
+
if (!location) return undefined;
|
|
58
|
+
return `${finding.ruleId.trim().toLowerCase()}\u0000${location}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizedPath(value: string): string {
|
|
62
|
+
return value.replaceAll("\\", "/").replace(/^\.\//, "").toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function findingLocations(finding: SecurityFinding) {
|
|
66
|
+
return finding.occurrences.flatMap(occurrence => occurrence.locations);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function taxonomyLocationMatch(reference: SecurityFinding, candidate: SecurityFinding): boolean {
|
|
70
|
+
const referenceCwes = new Set(reference.taxonomy.cwe.map(value => value.trim().toUpperCase()));
|
|
71
|
+
if (
|
|
72
|
+
referenceCwes.size === 0 ||
|
|
73
|
+
!candidate.taxonomy.cwe.some(value => referenceCwes.has(value.trim().toUpperCase()))
|
|
74
|
+
) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
for (const referenceLocation of findingLocations(reference)) {
|
|
78
|
+
for (const candidateLocation of findingLocations(candidate)) {
|
|
79
|
+
if (normalizedPath(referenceLocation.path) !== normalizedPath(candidateLocation.path)) continue;
|
|
80
|
+
const referenceEnd = referenceLocation.endLine ?? referenceLocation.startLine;
|
|
81
|
+
const candidateEnd = candidateLocation.endLine ?? candidateLocation.startLine;
|
|
82
|
+
if (
|
|
83
|
+
Math.max(referenceLocation.startLine, candidateLocation.startLine) <=
|
|
84
|
+
Math.min(referenceEnd, candidateEnd) ||
|
|
85
|
+
Math.abs(referenceLocation.startLine - candidateLocation.startLine) <= 3
|
|
86
|
+
) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findingSummary(finding: SecurityFinding): SecurityDifferentialFindingSummary {
|
|
95
|
+
const location = finding.occurrences.flatMap(occurrence => occurrence.locations)[0];
|
|
96
|
+
return {
|
|
97
|
+
findingId: finding.id,
|
|
98
|
+
ruleId: finding.ruleId,
|
|
99
|
+
title: finding.title,
|
|
100
|
+
severity: finding.severity.level,
|
|
101
|
+
confidence: finding.confidence.level,
|
|
102
|
+
validationStatus: finding.validation.status,
|
|
103
|
+
dispositionStatus: finding.disposition.status,
|
|
104
|
+
...(location ? { primaryLocation: { path: location.path, startLine: location.startLine } } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function scanSummary(bundle: SecurityScanBundle): SecurityDifferentialScanSummary {
|
|
109
|
+
return {
|
|
110
|
+
scanId: bundle.scan.id,
|
|
111
|
+
producer: bundle.scan.producer,
|
|
112
|
+
status: bundle.scan.status,
|
|
113
|
+
findingCount: bundle.findings.length,
|
|
114
|
+
actionableFindingCount: bundle.findings.filter(finding => finding.disposition.status === "open").length,
|
|
115
|
+
validatedFindingCount: bundle.findings.filter(finding => finding.validation.status === "validated").length,
|
|
116
|
+
rejectedFindingCount: bundle.findings.filter(finding => finding.validation.status === "rejected").length,
|
|
117
|
+
coverage: bundle.scan.coverage,
|
|
118
|
+
...(bundle.scan.metrics ? { metrics: bundle.scan.metrics } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ratio(numerator: number, denominator: number): number {
|
|
123
|
+
return denominator === 0 ? (numerator === 0 ? 1 : 0) : numerator / denominator;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function compareSecurityProducers(
|
|
127
|
+
reference: SecurityScanBundle,
|
|
128
|
+
candidate: SecurityScanBundle,
|
|
129
|
+
): SecurityDifferentialReport {
|
|
130
|
+
const candidateByFingerprint = new Map(candidate.findings.map(finding => [finding.fingerprint, finding]));
|
|
131
|
+
const candidateByFallback = new Map<string, SecurityFinding[]>();
|
|
132
|
+
for (const finding of candidate.findings) {
|
|
133
|
+
const key = fallbackKey(finding);
|
|
134
|
+
if (!key) continue;
|
|
135
|
+
const bucket = candidateByFallback.get(key) ?? [];
|
|
136
|
+
bucket.push(finding);
|
|
137
|
+
candidateByFallback.set(key, bucket);
|
|
138
|
+
}
|
|
139
|
+
const usedCandidateIds = new Set<string>();
|
|
140
|
+
const matchedReferenceIds = new Set<string>();
|
|
141
|
+
const matches: SecurityDifferentialFindingMatch[] = [];
|
|
142
|
+
const addMatch = (
|
|
143
|
+
referenceFinding: SecurityFinding,
|
|
144
|
+
candidateFinding: SecurityFinding,
|
|
145
|
+
basis: SecurityDifferentialFindingMatch["basis"],
|
|
146
|
+
) => {
|
|
147
|
+
matchedReferenceIds.add(referenceFinding.id);
|
|
148
|
+
usedCandidateIds.add(candidateFinding.id);
|
|
149
|
+
matches.push({
|
|
150
|
+
referenceFindingId: referenceFinding.id,
|
|
151
|
+
candidateFindingId: candidateFinding.id,
|
|
152
|
+
basis,
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
for (const referenceFinding of reference.findings) {
|
|
156
|
+
const exact = candidateByFingerprint.get(referenceFinding.fingerprint);
|
|
157
|
+
if (exact && !usedCandidateIds.has(exact.id)) addMatch(referenceFinding, exact, "fingerprint");
|
|
158
|
+
}
|
|
159
|
+
for (const referenceFinding of reference.findings) {
|
|
160
|
+
if (matchedReferenceIds.has(referenceFinding.id)) continue;
|
|
161
|
+
const key = fallbackKey(referenceFinding);
|
|
162
|
+
const fallback = key
|
|
163
|
+
? candidateByFallback.get(key)?.find(finding => !usedCandidateIds.has(finding.id))
|
|
164
|
+
: undefined;
|
|
165
|
+
if (fallback) addMatch(referenceFinding, fallback, "rule_location");
|
|
166
|
+
}
|
|
167
|
+
const unmatchedReferences = reference.findings.filter(finding => !matchedReferenceIds.has(finding.id));
|
|
168
|
+
for (const referenceFinding of unmatchedReferences) {
|
|
169
|
+
const compatibleCandidates = candidate.findings.filter(
|
|
170
|
+
finding => !usedCandidateIds.has(finding.id) && taxonomyLocationMatch(referenceFinding, finding),
|
|
171
|
+
);
|
|
172
|
+
if (compatibleCandidates.length !== 1) continue;
|
|
173
|
+
const compatibleCandidate = compatibleCandidates[0];
|
|
174
|
+
const compatibleReferences = unmatchedReferences.filter(
|
|
175
|
+
finding => !matchedReferenceIds.has(finding.id) && taxonomyLocationMatch(finding, compatibleCandidate),
|
|
176
|
+
);
|
|
177
|
+
if (compatibleReferences.length !== 1) continue;
|
|
178
|
+
addMatch(referenceFinding, compatibleCandidate, "taxonomy_location");
|
|
179
|
+
}
|
|
180
|
+
const referenceOnlyFindingIds = reference.findings
|
|
181
|
+
.filter(finding => !matchedReferenceIds.has(finding.id))
|
|
182
|
+
.map(finding => finding.id);
|
|
183
|
+
const candidateOnlyFindingIds = candidate.findings
|
|
184
|
+
.filter(finding => !usedCandidateIds.has(finding.id))
|
|
185
|
+
.map(finding => finding.id);
|
|
186
|
+
const unionSize = reference.findings.length + candidate.findings.length - matches.length;
|
|
187
|
+
return {
|
|
188
|
+
referenceScanId: reference.scan.id,
|
|
189
|
+
candidateScanId: candidate.scan.id,
|
|
190
|
+
matches,
|
|
191
|
+
referenceOnlyFindingIds,
|
|
192
|
+
reference: scanSummary(reference),
|
|
193
|
+
candidate: scanSummary(candidate),
|
|
194
|
+
referenceOnlyFindings: referenceOnlyFindingIds.map(findingId =>
|
|
195
|
+
findingSummary(reference.findings.find(finding => finding.id === findingId)!),
|
|
196
|
+
),
|
|
197
|
+
candidateOnlyFindings: candidateOnlyFindingIds.map(findingId =>
|
|
198
|
+
findingSummary(candidate.findings.find(finding => finding.id === findingId)!),
|
|
199
|
+
),
|
|
200
|
+
candidateOnlyFindingIds,
|
|
201
|
+
referenceFindingCount: reference.findings.length,
|
|
202
|
+
candidateFindingCount: candidate.findings.length,
|
|
203
|
+
matchedFindingCount: matches.length,
|
|
204
|
+
recallAgainstReference: ratio(matches.length, reference.findings.length),
|
|
205
|
+
precisionAgainstReference: ratio(matches.length, candidate.findings.length),
|
|
206
|
+
jaccardOverlap: ratio(matches.length, unionSize),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function compareSecurityLineage(
|
|
211
|
+
before: SecurityScanBundle,
|
|
212
|
+
after: SecurityScanBundle,
|
|
213
|
+
): SecurityComparisonReport {
|
|
214
|
+
// An incomplete after-scan proves nothing about unmatched earlier findings;
|
|
215
|
+
// claiming them "resolved" against a cancelled/partial/failed run would be a lie.
|
|
216
|
+
if (after.scan.status !== "completed") {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Security lineage comparison requires a completed after-scan; ${after.scan.id} is ${after.scan.status}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const differential = compareSecurityProducers(before, after);
|
|
222
|
+
const beforeById = new Map(before.findings.map(finding => [finding.id, finding]));
|
|
223
|
+
const afterById = new Map(after.findings.map(finding => [finding.id, finding]));
|
|
224
|
+
const matches: SecurityFindingMatch[] = differential.matches.map(match => {
|
|
225
|
+
const beforeFinding = beforeById.get(match.referenceFindingId);
|
|
226
|
+
const afterFinding = afterById.get(match.candidateFindingId);
|
|
227
|
+
if (!beforeFinding || !afterFinding) throw new Error("Security comparison produced an invalid finding reference");
|
|
228
|
+
return {
|
|
229
|
+
beforeFindingId: beforeFinding.id,
|
|
230
|
+
afterFindingId: afterFinding.id,
|
|
231
|
+
fingerprint: beforeFinding.fingerprint,
|
|
232
|
+
status: "unchanged",
|
|
233
|
+
matchBasis: match.basis,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
for (const findingId of differential.referenceOnlyFindingIds) {
|
|
237
|
+
const finding = beforeById.get(findingId);
|
|
238
|
+
if (!finding) continue;
|
|
239
|
+
matches.push({ beforeFindingId: finding.id, fingerprint: finding.fingerprint, status: "resolved" });
|
|
240
|
+
}
|
|
241
|
+
for (const findingId of differential.candidateOnlyFindingIds) {
|
|
242
|
+
const finding = afterById.get(findingId);
|
|
243
|
+
if (!finding) continue;
|
|
244
|
+
matches.push({ afterFindingId: finding.id, fingerprint: finding.fingerprint, status: "new" });
|
|
245
|
+
}
|
|
246
|
+
matches.sort((left, right) => left.fingerprint.localeCompare(right.fingerprint));
|
|
247
|
+
return {
|
|
248
|
+
beforeScanId: before.scan.id,
|
|
249
|
+
afterScanId: after.scan.id,
|
|
250
|
+
matches,
|
|
251
|
+
unchanged: matches.filter(match => match.status === "unchanged").length,
|
|
252
|
+
introduced: matches.filter(match => match.status === "new").length,
|
|
253
|
+
resolved: matches.filter(match => match.status === "resolved").length,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { SecurityLocation } from "./types";
|
|
2
|
+
|
|
3
|
+
function canonicalize(value: unknown): unknown {
|
|
4
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
5
|
+
if (!value || typeof value !== "object") return value;
|
|
6
|
+
const record = value as Record<string, unknown>;
|
|
7
|
+
const result: Record<string, unknown> = {};
|
|
8
|
+
for (const key of Object.keys(record).sort()) {
|
|
9
|
+
const item = record[key];
|
|
10
|
+
if (item !== undefined) result[key] = canonicalize(item);
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function canonicalSecurityJson(value: unknown): string {
|
|
16
|
+
return JSON.stringify(canonicalize(value));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeFingerprintPath(value: string): string {
|
|
20
|
+
return value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compareNormalizedLocationValues(
|
|
24
|
+
left: string | number | undefined,
|
|
25
|
+
right: string | number | undefined,
|
|
26
|
+
): number {
|
|
27
|
+
if (left === right) return 0;
|
|
28
|
+
if (left === undefined) return -1;
|
|
29
|
+
if (right === undefined) return 1;
|
|
30
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
31
|
+
const leftNaN = Number.isNaN(left);
|
|
32
|
+
const rightNaN = Number.isNaN(right);
|
|
33
|
+
if (leftNaN || rightNaN) return leftNaN ? (rightNaN ? 0 : -1) : 1;
|
|
34
|
+
return left - right;
|
|
35
|
+
}
|
|
36
|
+
return String(left) < String(right) ? -1 : 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const NORMALIZED_LOCATION_SORT_KEYS = ["path", "startLine", "endLine", "startColumn", "endColumn", "role"] as const;
|
|
40
|
+
|
|
41
|
+
function normalizedLocations(
|
|
42
|
+
locations: readonly SecurityLocation[],
|
|
43
|
+
): Array<Record<string, string | number | undefined>> {
|
|
44
|
+
return locations
|
|
45
|
+
.map(location => ({
|
|
46
|
+
path: normalizeFingerprintPath(location.path),
|
|
47
|
+
startLine: location.startLine,
|
|
48
|
+
endLine: location.endLine,
|
|
49
|
+
startColumn: location.startColumn,
|
|
50
|
+
endColumn: location.endColumn,
|
|
51
|
+
role: location.role,
|
|
52
|
+
}))
|
|
53
|
+
.sort((left, right) => {
|
|
54
|
+
for (const key of NORMALIZED_LOCATION_SORT_KEYS) {
|
|
55
|
+
const comparison = compareNormalizedLocationValues(left[key], right[key]);
|
|
56
|
+
if (comparison !== 0) return comparison;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SecurityFindingFingerprintInput {
|
|
63
|
+
ruleId: string;
|
|
64
|
+
category: string;
|
|
65
|
+
anchor?: string;
|
|
66
|
+
locations: readonly SecurityLocation[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createSecurityFindingFingerprint(input: SecurityFindingFingerprintInput): string {
|
|
70
|
+
const digest = Bun.SHA256.hash(
|
|
71
|
+
canonicalSecurityJson({
|
|
72
|
+
ruleId: input.ruleId.trim().toLowerCase(),
|
|
73
|
+
category: input.category.trim().toLowerCase(),
|
|
74
|
+
anchor: input.anchor?.trim().toLowerCase() || undefined,
|
|
75
|
+
locations: normalizedLocations(input.locations),
|
|
76
|
+
}),
|
|
77
|
+
"hex",
|
|
78
|
+
);
|
|
79
|
+
return `omp-security/v1:sha256:${digest}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createSecurityFindingId(fingerprint: string): string {
|
|
83
|
+
return `secf_${Bun.SHA256.hash(fingerprint, "hex").slice(0, 24)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createSecurityOccurrenceId(fingerprint: string, locations: readonly SecurityLocation[]): string {
|
|
87
|
+
const material = canonicalSecurityJson({ fingerprint, locations: normalizedLocations(locations) });
|
|
88
|
+
return `seco_${Bun.SHA256.hash(material, "hex").slice(0, 24)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createSecurityEvidenceId(fingerprint: string, label: string, ordinal: number): string {
|
|
92
|
+
return `sece_${Bun.SHA256.hash(canonicalSecurityJson({ fingerprint, label, ordinal }), "hex").slice(0, 24)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createSecurityScanId(randomUuid: () => string = () => Bun.randomUUIDv7()): string {
|
|
96
|
+
return `secscan_${randomUuid().replaceAll("-", "")}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createSecurityPlanId(fingerprint: string): string {
|
|
100
|
+
return `secplan_${Bun.SHA256.hash(fingerprint, "hex").slice(0, 24)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function encodeSecurityProjectKey(repositoryRoot: string): string {
|
|
104
|
+
const normalized = repositoryRoot.replaceAll("\\", "/").replace(/\/$/, "");
|
|
105
|
+
const readable = normalized
|
|
106
|
+
.replace(/^\//, "")
|
|
107
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
108
|
+
.replace(/^-+|-+$/g, "")
|
|
109
|
+
.slice(-80);
|
|
110
|
+
return `${readable || "project"}-${Bun.SHA256.hash(normalized, "hex").slice(0, 12)}`;
|
|
111
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
const stringRecordSchema = type({ "[string]": "string" });
|
|
4
|
+
const unknownRecordSchema = type({ "[string]": "unknown" });
|
|
5
|
+
|
|
6
|
+
export const securityProducerSchema = type({
|
|
7
|
+
kind: "'omp-native' | 'codex-security-bundle' | 'codex-security-cloud' | 'sarif-import'",
|
|
8
|
+
name: "string > 0",
|
|
9
|
+
"version?": "string",
|
|
10
|
+
"vendor?": "string",
|
|
11
|
+
"revision?": "string",
|
|
12
|
+
"pluginVersion?": "string",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const securityProvenanceSchema = type({
|
|
16
|
+
producer: securityProducerSchema,
|
|
17
|
+
createdAt: "string > 0",
|
|
18
|
+
"importedAt?": "string",
|
|
19
|
+
"sourceIds?": stringRecordSchema,
|
|
20
|
+
"vendorFingerprints?": stringRecordSchema,
|
|
21
|
+
"upstream?": {
|
|
22
|
+
"repository?": "string",
|
|
23
|
+
"revision?": "string",
|
|
24
|
+
"packageVersion?": "string",
|
|
25
|
+
"pluginVersion?": "string",
|
|
26
|
+
"archiveSha256?": "string",
|
|
27
|
+
},
|
|
28
|
+
"metadata?": unknownRecordSchema,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const securityLocationSchema = type({
|
|
32
|
+
path: "string > 0",
|
|
33
|
+
startLine: "number.integer >= 1",
|
|
34
|
+
"endLine?": "number.integer >= 1",
|
|
35
|
+
"startColumn?": "number.integer >= 1",
|
|
36
|
+
"endColumn?": "number.integer >= 1",
|
|
37
|
+
"role?": "string",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const securityEvidenceSchema = type({
|
|
41
|
+
id: "string > 0",
|
|
42
|
+
kind: "'code' | 'trace' | 'validation' | 'note'",
|
|
43
|
+
label: "string > 0",
|
|
44
|
+
explanation: "string",
|
|
45
|
+
"location?": securityLocationSchema,
|
|
46
|
+
"excerpt?": "string",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const securityOccurrenceSchema = type({
|
|
50
|
+
id: "string > 0",
|
|
51
|
+
locations: securityLocationSchema.array().atLeastLength(1),
|
|
52
|
+
evidenceIds: "string[]",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const securityFindingSchema = type({
|
|
56
|
+
id: "string > 0",
|
|
57
|
+
scanId: "string > 0",
|
|
58
|
+
fingerprint: "string > 0",
|
|
59
|
+
ruleId: "string > 0",
|
|
60
|
+
"anchor?": "string",
|
|
61
|
+
title: "string > 0",
|
|
62
|
+
summary: "string",
|
|
63
|
+
severity: {
|
|
64
|
+
level: "'critical' | 'high' | 'medium' | 'low' | 'informational'",
|
|
65
|
+
"score?": "number",
|
|
66
|
+
"scoringSystem?": "string",
|
|
67
|
+
"vector?": "string",
|
|
68
|
+
"rationale?": "string",
|
|
69
|
+
},
|
|
70
|
+
confidence: {
|
|
71
|
+
level: "'high' | 'medium' | 'low'",
|
|
72
|
+
"rationale?": "string",
|
|
73
|
+
},
|
|
74
|
+
taxonomy: {
|
|
75
|
+
category: "string > 0",
|
|
76
|
+
cwe: "string[]",
|
|
77
|
+
"tags?": "string[]",
|
|
78
|
+
},
|
|
79
|
+
occurrences: securityOccurrenceSchema.array().atLeastLength(1),
|
|
80
|
+
evidence: securityEvidenceSchema.array(),
|
|
81
|
+
"remediation?": "string",
|
|
82
|
+
validation: {
|
|
83
|
+
status: "'unvalidated' | 'validated' | 'rejected' | 'partial' | 'error'",
|
|
84
|
+
"summary?": "string",
|
|
85
|
+
evidenceIds: "string[]",
|
|
86
|
+
"validatedAt?": "string",
|
|
87
|
+
},
|
|
88
|
+
disposition: {
|
|
89
|
+
status: "'open' | 'false_positive' | 'accepted_risk' | 'fixed' | 'wont_fix'",
|
|
90
|
+
"rationale?": "string",
|
|
91
|
+
"updatedAt?": "string",
|
|
92
|
+
"actor?": "string",
|
|
93
|
+
},
|
|
94
|
+
provenance: securityProvenanceSchema,
|
|
95
|
+
"extensions?": unknownRecordSchema,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const securityCoverageSchema = type({
|
|
99
|
+
mode: "'repository' | 'scoped_path' | 'diff' | 'working_tree' | 'deep_repository' | 'imported'",
|
|
100
|
+
completeness: "'complete' | 'partial' | 'unknown'",
|
|
101
|
+
inventoryStrategy: "'repository' | 'scoped_path' | 'diff' | 'directory' | 'custom' | 'imported'",
|
|
102
|
+
includePaths: "string[]",
|
|
103
|
+
excludePaths: "string[]",
|
|
104
|
+
surfaces: type({
|
|
105
|
+
id: "string > 0",
|
|
106
|
+
label: "string > 0",
|
|
107
|
+
disposition: "'reported' | 'no_issue_found' | 'rejected' | 'not_applicable' | 'needs_follow_up'",
|
|
108
|
+
receiptRefs: "string[]",
|
|
109
|
+
"riskArea?": "string",
|
|
110
|
+
"notes?": "string",
|
|
111
|
+
}).array(),
|
|
112
|
+
explicitExclusions: type({ pattern: "string", reason: "string" }).array(),
|
|
113
|
+
deferred: type({
|
|
114
|
+
id: "string > 0",
|
|
115
|
+
reason: "string > 0",
|
|
116
|
+
"paths?": "string[]",
|
|
117
|
+
"surfaceIds?": "string[]",
|
|
118
|
+
}).array(),
|
|
119
|
+
"openQuestions?": type({ question: "string > 0", "followUpPrompt?": "string" }).array(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const securityTargetSchema = type({
|
|
123
|
+
kind: "'repository' | 'scoped_path' | 'ref_diff' | 'working_tree' | 'imported'",
|
|
124
|
+
repositoryRoot: "string > 0",
|
|
125
|
+
displayName: "string > 0",
|
|
126
|
+
"revision?": "string",
|
|
127
|
+
"baseRevision?": "string",
|
|
128
|
+
"headRevision?": "string",
|
|
129
|
+
includePaths: "string[]",
|
|
130
|
+
excludePaths: "string[]",
|
|
131
|
+
treeDigest: "string > 0",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const securityScanPlanSchema = type({
|
|
135
|
+
documentType: "'omp-security.scan-plan'",
|
|
136
|
+
schemaVersion: "'1.0'",
|
|
137
|
+
id: "string > 0",
|
|
138
|
+
createdAt: "string > 0",
|
|
139
|
+
repositoryRoot: "string > 0",
|
|
140
|
+
target: securityTargetSchema,
|
|
141
|
+
knowledgeBases: type({ path: "string > 0", sha256: "string > 0", size: "number.integer >= 0" }).array(),
|
|
142
|
+
output: {
|
|
143
|
+
root: "string > 0",
|
|
144
|
+
archiveExisting: "boolean",
|
|
145
|
+
existingState: "'absent' | 'empty' | 'archivable'",
|
|
146
|
+
},
|
|
147
|
+
model: { provider: "string > 0", modelId: "string > 0", "thinkingLevel?": "string" },
|
|
148
|
+
account: {
|
|
149
|
+
provider: "string > 0",
|
|
150
|
+
credentialId: "number.integer >= 1",
|
|
151
|
+
"accountId?": "string",
|
|
152
|
+
"email?": "string",
|
|
153
|
+
"organizationId?": "string",
|
|
154
|
+
"organizationName?": "string",
|
|
155
|
+
},
|
|
156
|
+
configFingerprint: "string > 0",
|
|
157
|
+
workflowFingerprint: "string > 0",
|
|
158
|
+
fingerprint: "string > 0",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export const securityScanMetricsSchema = type({
|
|
162
|
+
"runtimeMs?": "number >= 0",
|
|
163
|
+
"tokenUsage?": {
|
|
164
|
+
input: "number >= 0",
|
|
165
|
+
output: "number >= 0",
|
|
166
|
+
reasoning: "number >= 0",
|
|
167
|
+
cacheRead: "number >= 0",
|
|
168
|
+
cacheWrite: "number >= 0",
|
|
169
|
+
total: "number >= 0",
|
|
170
|
+
},
|
|
171
|
+
"cost?": "number >= 0",
|
|
172
|
+
"premiumRequests?": "number >= 0",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export const securityScanSchema = type({
|
|
176
|
+
documentType: "'omp-security.scan'",
|
|
177
|
+
schemaVersion: "'1.0'",
|
|
178
|
+
id: "string > 0",
|
|
179
|
+
projectKey: "string > 0",
|
|
180
|
+
status: "'planned' | 'running' | 'completed' | 'partial' | 'cancelled' | 'failed'",
|
|
181
|
+
createdAt: "string > 0",
|
|
182
|
+
"startedAt?": "string",
|
|
183
|
+
"completedAt?": "string",
|
|
184
|
+
"plan?": securityScanPlanSchema,
|
|
185
|
+
target: securityTargetSchema,
|
|
186
|
+
producer: securityProducerSchema,
|
|
187
|
+
provenance: securityProvenanceSchema,
|
|
188
|
+
findingIds: "string[]",
|
|
189
|
+
coverage: securityCoverageSchema,
|
|
190
|
+
"reportRef?": "string",
|
|
191
|
+
"sarifRef?": "string",
|
|
192
|
+
"error?": "string",
|
|
193
|
+
"metrics?": securityScanMetricsSchema,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export const securityScanBundleSchema = type({
|
|
197
|
+
scan: securityScanSchema,
|
|
198
|
+
findings: securityFindingSchema.array(),
|
|
199
|
+
"report?": "string",
|
|
200
|
+
"sarif?": unknownRecordSchema,
|
|
201
|
+
});
|