@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,387 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
SecurityCoverage,
|
|
5
|
+
SecurityEvidence,
|
|
6
|
+
SecurityFinding,
|
|
7
|
+
SecurityLocation,
|
|
8
|
+
SecurityProducer,
|
|
9
|
+
SecurityProvenance,
|
|
10
|
+
SecurityScan,
|
|
11
|
+
SecurityScanBundle,
|
|
12
|
+
SecurityTarget,
|
|
13
|
+
SecurityUpstreamProvenance,
|
|
14
|
+
} from "../contracts";
|
|
15
|
+
import {
|
|
16
|
+
createSecurityEvidenceId,
|
|
17
|
+
createSecurityFindingFingerprint,
|
|
18
|
+
createSecurityFindingId,
|
|
19
|
+
createSecurityOccurrenceId,
|
|
20
|
+
createSecurityScanId,
|
|
21
|
+
encodeSecurityProjectKey,
|
|
22
|
+
parseSecurityScanBundle,
|
|
23
|
+
} from "../contracts";
|
|
24
|
+
|
|
25
|
+
interface CodexManifest {
|
|
26
|
+
documentType?: string;
|
|
27
|
+
schemaVersion?: string;
|
|
28
|
+
scan?: {
|
|
29
|
+
id?: string;
|
|
30
|
+
producer?: { name?: string; version?: string };
|
|
31
|
+
status?: string;
|
|
32
|
+
startedAt?: string;
|
|
33
|
+
completedAt?: string;
|
|
34
|
+
target?: Record<string, unknown>;
|
|
35
|
+
scope?: { includePaths?: unknown; excludePaths?: unknown };
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CodexFinding {
|
|
40
|
+
findingId?: string;
|
|
41
|
+
occurrenceId?: string;
|
|
42
|
+
ruleId?: string;
|
|
43
|
+
identity?: { anchor?: string };
|
|
44
|
+
fingerprints?: { algorithm?: string; primary?: string };
|
|
45
|
+
title?: string;
|
|
46
|
+
summary?: string;
|
|
47
|
+
severity?: { level?: string; score?: number; scoringSystem?: string; vector?: string; rationale?: string };
|
|
48
|
+
confidence?: { level?: string; rationale?: string };
|
|
49
|
+
taxonomy?: { category?: string; cwe?: unknown };
|
|
50
|
+
locations?: Array<{ path?: string; startLine?: number; endLine?: number; role?: string }>;
|
|
51
|
+
codeEvidence?: Array<{
|
|
52
|
+
id?: string;
|
|
53
|
+
label?: string;
|
|
54
|
+
path?: string;
|
|
55
|
+
startLine?: number;
|
|
56
|
+
endLine?: number;
|
|
57
|
+
role?: string;
|
|
58
|
+
code?: string;
|
|
59
|
+
explanation?: string;
|
|
60
|
+
}>;
|
|
61
|
+
remediation?: string;
|
|
62
|
+
validation?: Record<string, unknown> | null;
|
|
63
|
+
provenance?: Record<string, unknown>;
|
|
64
|
+
extensions?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface CodexFindingsDocument {
|
|
68
|
+
documentType?: string;
|
|
69
|
+
schemaVersion?: string;
|
|
70
|
+
scanId?: string;
|
|
71
|
+
findings?: CodexFinding[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface CodexCoverageDocument {
|
|
75
|
+
documentType?: string;
|
|
76
|
+
schemaVersion?: string;
|
|
77
|
+
scanId?: string;
|
|
78
|
+
mode?: string;
|
|
79
|
+
completeness?: string;
|
|
80
|
+
inventoryStrategy?: string;
|
|
81
|
+
includePaths?: unknown;
|
|
82
|
+
excludePaths?: unknown;
|
|
83
|
+
surfaces?: unknown;
|
|
84
|
+
explicitExclusions?: unknown;
|
|
85
|
+
deferred?: unknown;
|
|
86
|
+
openQuestions?: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface CodexFixtureProvenance {
|
|
90
|
+
repository?: string;
|
|
91
|
+
revision?: string;
|
|
92
|
+
packageVersion?: string;
|
|
93
|
+
pluginVersion?: string;
|
|
94
|
+
archiveSha256?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CodexSecurityImportOptions {
|
|
98
|
+
repositoryRoot: string;
|
|
99
|
+
createdAt?: string;
|
|
100
|
+
createScanId?: () => string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readJson<T>(filePath: string): Promise<T> {
|
|
104
|
+
return JSON.parse(await Bun.file(filePath).text()) as T;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stringArray(value: unknown): string[] {
|
|
108
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function importedLocationPath(value: string): string {
|
|
112
|
+
const normalized = value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
113
|
+
if (
|
|
114
|
+
!normalized ||
|
|
115
|
+
normalized.startsWith("/") ||
|
|
116
|
+
/^[a-zA-Z]:\//.test(normalized) ||
|
|
117
|
+
normalized.split("/").includes("..")
|
|
118
|
+
) {
|
|
119
|
+
throw new Error(`Codex Security bundle locations must be repository-relative: ${value}`);
|
|
120
|
+
}
|
|
121
|
+
return normalized;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function locationsForFinding(finding: CodexFinding): SecurityLocation[] {
|
|
125
|
+
const locations: SecurityLocation[] = [];
|
|
126
|
+
for (const location of finding.locations ?? []) {
|
|
127
|
+
if (typeof location.path !== "string" || typeof location.startLine !== "number") continue;
|
|
128
|
+
const normalized: SecurityLocation = {
|
|
129
|
+
path: importedLocationPath(location.path),
|
|
130
|
+
startLine: location.startLine,
|
|
131
|
+
};
|
|
132
|
+
if (location.endLine !== undefined) normalized.endLine = location.endLine;
|
|
133
|
+
if (location.role !== undefined) normalized.role = location.role;
|
|
134
|
+
locations.push(normalized);
|
|
135
|
+
}
|
|
136
|
+
return locations.length > 0 ? locations : [{ path: "unknown", startLine: 1, role: "unknown" }];
|
|
137
|
+
}
|
|
138
|
+
const canonicalValidationStatuses: Record<string, true> = {
|
|
139
|
+
unvalidated: true,
|
|
140
|
+
validated: true,
|
|
141
|
+
rejected: true,
|
|
142
|
+
partial: true,
|
|
143
|
+
error: true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function validationStatus(
|
|
147
|
+
validation: Record<string, unknown> | null | undefined,
|
|
148
|
+
): SecurityFinding["validation"]["status"] {
|
|
149
|
+
const status = validation?.status;
|
|
150
|
+
return typeof status === "string" && canonicalValidationStatuses[status] === true
|
|
151
|
+
? (status as SecurityFinding["validation"]["status"])
|
|
152
|
+
: "unvalidated";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function mapCoverage(document: CodexCoverageDocument): SecurityCoverage {
|
|
156
|
+
const allowedModes = new Set(["repository", "scoped_path", "diff", "working_tree", "deep_repository"]);
|
|
157
|
+
const mode = allowedModes.has(document.mode ?? "")
|
|
158
|
+
? (document.mode as SecurityCoverage["mode"])
|
|
159
|
+
: document.mode === "commit" || document.mode === "branch_diff"
|
|
160
|
+
? "diff"
|
|
161
|
+
: "imported";
|
|
162
|
+
const completeness = ["complete", "partial", "unknown"].includes(document.completeness ?? "")
|
|
163
|
+
? (document.completeness as SecurityCoverage["completeness"])
|
|
164
|
+
: "unknown";
|
|
165
|
+
const inventoryStrategy = ["repository", "scoped_path", "diff", "directory", "custom"].includes(
|
|
166
|
+
document.inventoryStrategy ?? "",
|
|
167
|
+
)
|
|
168
|
+
? (document.inventoryStrategy as SecurityCoverage["inventoryStrategy"])
|
|
169
|
+
: "imported";
|
|
170
|
+
const coverage: SecurityCoverage = {
|
|
171
|
+
mode,
|
|
172
|
+
completeness,
|
|
173
|
+
inventoryStrategy,
|
|
174
|
+
includePaths: stringArray(document.includePaths),
|
|
175
|
+
excludePaths: stringArray(document.excludePaths),
|
|
176
|
+
surfaces: Array.isArray(document.surfaces) ? (document.surfaces as SecurityCoverage["surfaces"]) : [],
|
|
177
|
+
explicitExclusions: Array.isArray(document.explicitExclusions)
|
|
178
|
+
? (document.explicitExclusions as SecurityCoverage["explicitExclusions"])
|
|
179
|
+
: [],
|
|
180
|
+
deferred: Array.isArray(document.deferred) ? (document.deferred as SecurityCoverage["deferred"]) : [],
|
|
181
|
+
};
|
|
182
|
+
if (Array.isArray(document.openQuestions)) {
|
|
183
|
+
coverage.openQuestions = document.openQuestions as SecurityCoverage["openQuestions"];
|
|
184
|
+
}
|
|
185
|
+
return coverage;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function importCodexSecurityBundle(
|
|
189
|
+
bundleDirectory: string,
|
|
190
|
+
options: CodexSecurityImportOptions,
|
|
191
|
+
): Promise<SecurityScanBundle> {
|
|
192
|
+
const root = path.resolve(bundleDirectory);
|
|
193
|
+
const manifest = await readJson<CodexManifest>(path.join(root, "scan-manifest.json"));
|
|
194
|
+
const findingsDocument = await readJson<CodexFindingsDocument>(path.join(root, "findings.json"));
|
|
195
|
+
const coverageDocument = await readJson<CodexCoverageDocument>(path.join(root, "coverage.json"));
|
|
196
|
+
if (manifest.documentType !== "codex-security.scan-manifest" || manifest.schemaVersion !== "1.0") {
|
|
197
|
+
throw new Error("Unsupported Codex Security scan manifest");
|
|
198
|
+
}
|
|
199
|
+
if (findingsDocument.documentType !== "codex-security.findings" || findingsDocument.schemaVersion !== "1.0") {
|
|
200
|
+
throw new Error("Unsupported Codex Security findings document");
|
|
201
|
+
}
|
|
202
|
+
if (coverageDocument.documentType !== "codex-security.coverage" || coverageDocument.schemaVersion !== "1.0") {
|
|
203
|
+
throw new Error("Unsupported Codex Security coverage document");
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
!manifest.scan?.id ||
|
|
207
|
+
findingsDocument.scanId !== manifest.scan.id ||
|
|
208
|
+
coverageDocument.scanId !== manifest.scan.id
|
|
209
|
+
) {
|
|
210
|
+
throw new Error("Codex Security bundle scan IDs do not agree");
|
|
211
|
+
}
|
|
212
|
+
const fixtureProvenance = await readJson<CodexFixtureProvenance>(path.join(root, "PROVENANCE.json")).catch(
|
|
213
|
+
(): CodexFixtureProvenance => ({}),
|
|
214
|
+
);
|
|
215
|
+
const scanId = options.createScanId?.() ?? createSecurityScanId();
|
|
216
|
+
const createdAt = options.createdAt ?? manifest.scan.startedAt ?? new Date().toISOString();
|
|
217
|
+
const canonicalRoot = await fs.realpath(path.resolve(options.repositoryRoot));
|
|
218
|
+
const producer: SecurityProducer = {
|
|
219
|
+
kind: "codex-security-bundle",
|
|
220
|
+
name: manifest.scan.producer?.name || "codex-security",
|
|
221
|
+
vendor: "openai",
|
|
222
|
+
};
|
|
223
|
+
if (manifest.scan.producer?.version !== undefined) producer.version = manifest.scan.producer.version;
|
|
224
|
+
if (fixtureProvenance.revision !== undefined) producer.revision = fixtureProvenance.revision;
|
|
225
|
+
if (fixtureProvenance.pluginVersion !== undefined) producer.pluginVersion = fixtureProvenance.pluginVersion;
|
|
226
|
+
const upstream: SecurityUpstreamProvenance = {};
|
|
227
|
+
if (fixtureProvenance.repository !== undefined) upstream.repository = fixtureProvenance.repository;
|
|
228
|
+
if (fixtureProvenance.revision !== undefined) upstream.revision = fixtureProvenance.revision;
|
|
229
|
+
if (fixtureProvenance.packageVersion !== undefined) upstream.packageVersion = fixtureProvenance.packageVersion;
|
|
230
|
+
if (fixtureProvenance.pluginVersion !== undefined) upstream.pluginVersion = fixtureProvenance.pluginVersion;
|
|
231
|
+
if (fixtureProvenance.archiveSha256 !== undefined) upstream.archiveSha256 = fixtureProvenance.archiveSha256;
|
|
232
|
+
const findings: SecurityFinding[] = [];
|
|
233
|
+
for (const source of findingsDocument.findings ?? []) {
|
|
234
|
+
const ruleId = source.ruleId || "codex-security.unknown";
|
|
235
|
+
const category = source.taxonomy?.category || ruleId.split(/[./-]/)[0] || "security";
|
|
236
|
+
const hasSourceLocations = (source.locations ?? []).some(
|
|
237
|
+
location => typeof location.path === "string" && typeof location.startLine === "number",
|
|
238
|
+
);
|
|
239
|
+
const locations = locationsForFinding(source);
|
|
240
|
+
const anchor =
|
|
241
|
+
source.identity?.anchor ||
|
|
242
|
+
source.fingerprints?.primary ||
|
|
243
|
+
(!hasSourceLocations ? source.findingId : undefined);
|
|
244
|
+
const fingerprint = createSecurityFindingFingerprint({
|
|
245
|
+
ruleId,
|
|
246
|
+
category,
|
|
247
|
+
anchor,
|
|
248
|
+
locations,
|
|
249
|
+
});
|
|
250
|
+
const evidence: SecurityEvidence[] = (source.codeEvidence ?? []).map((item, index) => {
|
|
251
|
+
const entry: SecurityEvidence = {
|
|
252
|
+
id: createSecurityEvidenceId(fingerprint, item.label || item.id || "code evidence", index),
|
|
253
|
+
kind: "code",
|
|
254
|
+
label: item.label || item.id || `Evidence ${index + 1}`,
|
|
255
|
+
explanation: item.explanation || "",
|
|
256
|
+
};
|
|
257
|
+
if (typeof item.path === "string" && typeof item.startLine === "number") {
|
|
258
|
+
const location: SecurityLocation = { path: importedLocationPath(item.path), startLine: item.startLine };
|
|
259
|
+
if (item.endLine !== undefined) location.endLine = item.endLine;
|
|
260
|
+
if (item.role !== undefined) location.role = item.role;
|
|
261
|
+
entry.location = location;
|
|
262
|
+
}
|
|
263
|
+
if (item.code !== undefined) entry.excerpt = item.code;
|
|
264
|
+
return entry;
|
|
265
|
+
});
|
|
266
|
+
const provenance: SecurityProvenance = {
|
|
267
|
+
producer,
|
|
268
|
+
createdAt,
|
|
269
|
+
importedAt: new Date().toISOString(),
|
|
270
|
+
sourceIds: {
|
|
271
|
+
scanId: manifest.scan.id,
|
|
272
|
+
...(source.findingId ? { findingId: source.findingId } : {}),
|
|
273
|
+
...(source.occurrenceId ? { occurrenceId: source.occurrenceId } : {}),
|
|
274
|
+
},
|
|
275
|
+
upstream,
|
|
276
|
+
};
|
|
277
|
+
if (source.fingerprints?.primary) {
|
|
278
|
+
provenance.vendorFingerprints = {
|
|
279
|
+
[source.fingerprints.algorithm || "codex-security/v1"]: source.fingerprints.primary,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (source.provenance !== undefined) provenance.metadata = source.provenance;
|
|
283
|
+
const finding: SecurityFinding = {
|
|
284
|
+
id: createSecurityFindingId(fingerprint),
|
|
285
|
+
scanId,
|
|
286
|
+
fingerprint,
|
|
287
|
+
ruleId,
|
|
288
|
+
title: source.title || ruleId,
|
|
289
|
+
summary: source.summary || "",
|
|
290
|
+
severity: {
|
|
291
|
+
level: ["critical", "high", "medium", "low", "informational"].includes(source.severity?.level ?? "")
|
|
292
|
+
? (source.severity?.level as SecurityFinding["severity"]["level"])
|
|
293
|
+
: "informational",
|
|
294
|
+
},
|
|
295
|
+
confidence: {
|
|
296
|
+
level: ["high", "medium", "low"].includes(source.confidence?.level ?? "")
|
|
297
|
+
? (source.confidence?.level as SecurityFinding["confidence"]["level"])
|
|
298
|
+
: "medium",
|
|
299
|
+
},
|
|
300
|
+
taxonomy: { category, cwe: stringArray(source.taxonomy?.cwe) },
|
|
301
|
+
occurrences: [
|
|
302
|
+
{
|
|
303
|
+
id: createSecurityOccurrenceId(fingerprint, locations),
|
|
304
|
+
locations,
|
|
305
|
+
evidenceIds: evidence.map(item => item.id),
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
evidence,
|
|
309
|
+
validation: {
|
|
310
|
+
status: validationStatus(source.validation),
|
|
311
|
+
evidenceIds: [],
|
|
312
|
+
},
|
|
313
|
+
disposition: { status: "open" },
|
|
314
|
+
provenance,
|
|
315
|
+
};
|
|
316
|
+
if (anchor !== undefined) finding.anchor = anchor;
|
|
317
|
+
if (source.severity?.score !== undefined) finding.severity.score = source.severity.score;
|
|
318
|
+
if (source.severity?.scoringSystem !== undefined) finding.severity.scoringSystem = source.severity.scoringSystem;
|
|
319
|
+
if (source.severity?.vector !== undefined) finding.severity.vector = source.severity.vector;
|
|
320
|
+
if (source.severity?.rationale !== undefined) finding.severity.rationale = source.severity.rationale;
|
|
321
|
+
if (source.confidence?.rationale !== undefined) finding.confidence.rationale = source.confidence.rationale;
|
|
322
|
+
if (source.remediation !== undefined) finding.remediation = source.remediation;
|
|
323
|
+
if (source.validation) finding.validation.summary = JSON.stringify(source.validation);
|
|
324
|
+
if (source.extensions !== undefined) finding.extensions = source.extensions;
|
|
325
|
+
findings.push(finding);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const target = manifest.scan.target ?? {};
|
|
329
|
+
const sourceKind = String(target.kind ?? "");
|
|
330
|
+
const targetKind =
|
|
331
|
+
sourceKind === "git_diff" ? "ref_diff" : sourceKind === "git_worktree" ? "working_tree" : "imported";
|
|
332
|
+
const reportPath = path.join(root, "report.md");
|
|
333
|
+
const sarifPath = path.join(root, "exports", "results.sarif");
|
|
334
|
+
const report = await Bun.file(reportPath)
|
|
335
|
+
.text()
|
|
336
|
+
.catch(() => undefined);
|
|
337
|
+
const sarifText = await Bun.file(sarifPath)
|
|
338
|
+
.text()
|
|
339
|
+
.catch(() => undefined);
|
|
340
|
+
const scanProvenance: SecurityProvenance = {
|
|
341
|
+
producer,
|
|
342
|
+
createdAt,
|
|
343
|
+
importedAt: new Date().toISOString(),
|
|
344
|
+
sourceIds: { scanId: manifest.scan.id },
|
|
345
|
+
upstream,
|
|
346
|
+
metadata: { bundleDirectory: root },
|
|
347
|
+
};
|
|
348
|
+
const canonicalTarget: SecurityTarget = {
|
|
349
|
+
kind: targetKind,
|
|
350
|
+
repositoryRoot: canonicalRoot,
|
|
351
|
+
displayName: String(target.displayName ?? path.basename(canonicalRoot)),
|
|
352
|
+
includePaths: stringArray(manifest.scan.scope?.includePaths),
|
|
353
|
+
excludePaths: stringArray(manifest.scan.scope?.excludePaths),
|
|
354
|
+
treeDigest:
|
|
355
|
+
typeof target.snapshotDigest === "string"
|
|
356
|
+
? target.snapshotDigest
|
|
357
|
+
: Bun.SHA256.hash(JSON.stringify({ manifest, findingsDocument, coverageDocument }), "hex"),
|
|
358
|
+
};
|
|
359
|
+
if (typeof target.revision === "string") canonicalTarget.revision = target.revision;
|
|
360
|
+
if (typeof target.baseRevision === "string") canonicalTarget.baseRevision = target.baseRevision;
|
|
361
|
+
if (typeof target.headRevision === "string") canonicalTarget.headRevision = target.headRevision;
|
|
362
|
+
const scan: SecurityScan = {
|
|
363
|
+
documentType: "omp-security.scan",
|
|
364
|
+
schemaVersion: "1.0",
|
|
365
|
+
id: scanId,
|
|
366
|
+
projectKey: encodeSecurityProjectKey(canonicalRoot),
|
|
367
|
+
status: "completed",
|
|
368
|
+
createdAt,
|
|
369
|
+
completedAt: manifest.scan.completedAt ?? createdAt,
|
|
370
|
+
target: canonicalTarget,
|
|
371
|
+
producer,
|
|
372
|
+
provenance: scanProvenance,
|
|
373
|
+
findingIds: findings.map(finding => finding.id),
|
|
374
|
+
coverage: mapCoverage(coverageDocument),
|
|
375
|
+
};
|
|
376
|
+
if (manifest.scan.startedAt !== undefined) scan.startedAt = manifest.scan.startedAt;
|
|
377
|
+
if (scan.startedAt !== undefined) {
|
|
378
|
+
const runtimeMs = new Date(scan.completedAt ?? createdAt).getTime() - new Date(scan.startedAt).getTime();
|
|
379
|
+
if (Number.isFinite(runtimeMs) && runtimeMs >= 0) scan.metrics = { runtimeMs };
|
|
380
|
+
}
|
|
381
|
+
if (report !== undefined) scan.reportRef = "report.md";
|
|
382
|
+
if (sarifText !== undefined) scan.sarifRef = "results.sarif";
|
|
383
|
+
const bundle: SecurityScanBundle = { scan, findings };
|
|
384
|
+
if (report !== undefined) bundle.report = report;
|
|
385
|
+
if (sarifText !== undefined) bundle.sarif = JSON.parse(sarifText) as Record<string, unknown>;
|
|
386
|
+
return parseSecurityScanBundle(bundle);
|
|
387
|
+
}
|