@oh-my-pi/pi-coding-agent 17.2.0 → 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 +18 -0
- package/dist/{CHANGELOG-be1f2t8h.md → CHANGELOG-dj46zzrm.md} +18 -0
- package/dist/cli.js +3340 -3195
- package/dist/types/config/model-discovery.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +23 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/security-protocol.d.ts +15 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/sdk.d.ts +17 -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/slash-commands/helpers/security.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +6 -1
- package/dist/types/tools/security-scan.d.ts +96 -0
- package/package.json +12 -12
- package/scripts/security-compare.ts +40 -0
- package/src/config/model-discovery.ts +32 -5
- package/src/config/model-registry.ts +4 -0
- package/src/config/settings-schema.ts +12 -0
- package/src/eval/py/prelude.py +7 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -0
- package/src/internal-urls/index.ts +1 -0
- 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/lsp/index.ts +3 -0
- package/src/modes/rpc/host-uris.ts +6 -0
- 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/system-prompt.md +3 -0
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/sdk.ts +34 -6
- 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/slash-commands/builtin-registry.ts +21 -0
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +3 -0
- package/src/task/structured-subagent.ts +6 -4
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/index.ts +9 -1
- package/src/tools/path-utils.ts +3 -0
- package/src/tools/security-scan.ts +287 -0
|
@@ -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
|
+
});
|