auditor-lambda 0.3.17 → 0.3.19
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/README.md +12 -0
- package/dist/cli.js +62 -55
- package/dist/extractors/browserExtension.d.ts +13 -0
- package/dist/extractors/browserExtension.js +378 -0
- package/dist/extractors/disposition.js +8 -1
- package/dist/extractors/fsIntake.js +1 -0
- package/dist/extractors/graph.js +6 -0
- package/dist/extractors/pathPatterns.js +15 -1
- package/dist/extractors/surfaces.d.ts +4 -1
- package/dist/extractors/surfaces.js +15 -2
- package/dist/orchestrator/autoFixExecutor.js +41 -10
- package/dist/orchestrator/internalExecutors.js +6 -2
- package/dist/orchestrator/planning.js +5 -1
- package/dist/orchestrator/syntaxResolutionExecutor.js +10 -1
- package/dist/orchestrator/unitBuilder.d.ts +3 -1
- package/dist/orchestrator/unitBuilder.js +19 -4
- package/dist/supervisor/operatorHandoff.js +1 -1
- package/docs/operator-guide.md +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,6 +149,18 @@ For task-to-coverage inspection without reverse-engineering multiple artifacts:
|
|
|
149
149
|
audit-code explain-task <task_id>
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
To remove a leftover `.audit-artifacts/` directory from an interrupted or
|
|
153
|
+
crashed audit:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
audit-code cleanup
|
|
157
|
+
audit-code cleanup --dry-run # preview without deleting
|
|
158
|
+
audit-code cleanup --force # delete even if state is unknown
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Refuses to delete if the audit state is `active` or `blocked` (resumable).
|
|
162
|
+
Pass `--force` when `audit_state.json` is missing (crashed run).
|
|
163
|
+
|
|
152
164
|
For a local stdio MCP server entrypoint:
|
|
153
165
|
|
|
154
166
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
3
|
import { Buffer } from "node:buffer";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -308,30 +308,6 @@ async function listBatchResultFiles(batchDir) {
|
|
|
308
308
|
}
|
|
309
309
|
return files;
|
|
310
310
|
}
|
|
311
|
-
const PROJECT_SIGNALS = [
|
|
312
|
-
"package.json",
|
|
313
|
-
"go.mod",
|
|
314
|
-
"Cargo.toml",
|
|
315
|
-
"pom.xml",
|
|
316
|
-
"build.gradle",
|
|
317
|
-
"pyproject.toml",
|
|
318
|
-
"setup.py",
|
|
319
|
-
"setup.cfg",
|
|
320
|
-
"Makefile",
|
|
321
|
-
"CMakeLists.txt",
|
|
322
|
-
];
|
|
323
|
-
async function detectProjectRoot(root) {
|
|
324
|
-
for (const signal of PROJECT_SIGNALS) {
|
|
325
|
-
try {
|
|
326
|
-
await access(join(root, signal));
|
|
327
|
-
return signal;
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
// not found, try next
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
311
|
function buildPendingAuditTasks(bundle) {
|
|
336
312
|
const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
|
|
337
313
|
const pendingTasks = (bundle.audit_tasks ?? []).filter((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id));
|
|
@@ -606,6 +582,7 @@ export async function runSample(argv = process.argv) {
|
|
|
606
582
|
async function cmdAdvanceAudit(argv) {
|
|
607
583
|
const root = getRootDir(argv);
|
|
608
584
|
const artifactsDir = getArtifactsDir(argv);
|
|
585
|
+
await cleanupStaleArtifactsDir(artifactsDir);
|
|
609
586
|
await mkdir(artifactsDir, { recursive: true });
|
|
610
587
|
await ensureSupervisorDirs(artifactsDir);
|
|
611
588
|
let sessionConfig;
|
|
@@ -685,6 +662,7 @@ async function cmdAdvanceAudit(argv) {
|
|
|
685
662
|
async function cmdRunToCompletion(argv) {
|
|
686
663
|
const root = getRootDir(argv);
|
|
687
664
|
const artifactsDir = getArtifactsDir(argv);
|
|
665
|
+
await cleanupStaleArtifactsDir(artifactsDir);
|
|
688
666
|
await mkdir(artifactsDir, { recursive: true });
|
|
689
667
|
await ensureSupervisorDirs(artifactsDir);
|
|
690
668
|
let sessionConfig;
|
|
@@ -714,35 +692,6 @@ async function cmdRunToCompletion(argv) {
|
|
|
714
692
|
let pendingBatchAuditResults = batchResultsDir
|
|
715
693
|
? await listBatchResultFiles(batchResultsDir)
|
|
716
694
|
: [];
|
|
717
|
-
const earlyBundle = await loadArtifactBundle(artifactsDir);
|
|
718
|
-
if (!earlyBundle.unit_manifest) {
|
|
719
|
-
const foundSignal = await detectProjectRoot(root);
|
|
720
|
-
if (!foundSignal) {
|
|
721
|
-
const blocker = `No recognisable project signals found in ${root}. Expected one of: ${PROJECT_SIGNALS.join(", ")}. Check that --root points to the repository root, not a subdirectory or an unrelated path.`;
|
|
722
|
-
const earlyState = deriveAuditState(earlyBundle);
|
|
723
|
-
const blockedState = buildBlockedAuditState({
|
|
724
|
-
state: earlyState,
|
|
725
|
-
obligationId: null,
|
|
726
|
-
executor: null,
|
|
727
|
-
blocker,
|
|
728
|
-
});
|
|
729
|
-
await emitEnvelope({
|
|
730
|
-
root,
|
|
731
|
-
artifactsDir,
|
|
732
|
-
bundle: { ...earlyBundle, audit_state: blockedState },
|
|
733
|
-
audit_state: blockedState,
|
|
734
|
-
selected_obligation: null,
|
|
735
|
-
selected_executor: null,
|
|
736
|
-
progress_made: false,
|
|
737
|
-
artifacts_written: [],
|
|
738
|
-
progress_summary: blocker,
|
|
739
|
-
next_likely_step: null,
|
|
740
|
-
providerName: provider.name,
|
|
741
|
-
isConfigError: true,
|
|
742
|
-
});
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
695
|
let pendingAuditResultsPath = getFlag(argv, "--results");
|
|
747
696
|
let pendingRuntimeUpdatesPath = getFlag(argv, "--updates");
|
|
748
697
|
let pendingExternalAnalyzerPath = getFlag(argv, "--external-analyzer-results");
|
|
@@ -2353,6 +2302,61 @@ async function cmdSynthesize(argv) {
|
|
|
2353
2302
|
progress_summary: result.progress_summary,
|
|
2354
2303
|
}, null, 2));
|
|
2355
2304
|
}
|
|
2305
|
+
async function cleanupStaleArtifactsDir(artifactsDir) {
|
|
2306
|
+
let status;
|
|
2307
|
+
try {
|
|
2308
|
+
const state = await readJsonFile(join(artifactsDir, "audit_state.json"));
|
|
2309
|
+
status = state.status;
|
|
2310
|
+
}
|
|
2311
|
+
catch (error) {
|
|
2312
|
+
if (!isFileMissingError(error)) {
|
|
2313
|
+
throw error;
|
|
2314
|
+
}
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
if (status === "complete" || status === "not_started") {
|
|
2318
|
+
await rm(artifactsDir, { recursive: true, force: true });
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
async function cmdCleanup(argv) {
|
|
2322
|
+
const artifactsDir = getArtifactsDir(argv);
|
|
2323
|
+
const dryRun = hasFlag(argv, "--dry-run");
|
|
2324
|
+
const force = hasFlag(argv, "--force");
|
|
2325
|
+
let status;
|
|
2326
|
+
try {
|
|
2327
|
+
const state = await readJsonFile(join(artifactsDir, "audit_state.json"));
|
|
2328
|
+
status = state.status;
|
|
2329
|
+
}
|
|
2330
|
+
catch (error) {
|
|
2331
|
+
if (!isFileMissingError(error)) {
|
|
2332
|
+
throw error;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
const resumable = status === "active" || status === "blocked";
|
|
2336
|
+
const unknown = status === undefined;
|
|
2337
|
+
if ((resumable || unknown) && !force) {
|
|
2338
|
+
const reason = resumable
|
|
2339
|
+
? `audit is ${status} and may be resumed`
|
|
2340
|
+
: "no audit_state.json found; artifacts may be from a crashed audit";
|
|
2341
|
+
console.log(JSON.stringify({
|
|
2342
|
+
artifacts_dir: artifactsDir,
|
|
2343
|
+
action: "skipped",
|
|
2344
|
+
reason: `${reason} — use --force to delete anyway`,
|
|
2345
|
+
dry_run: dryRun,
|
|
2346
|
+
}, null, 2));
|
|
2347
|
+
process.exitCode = 1;
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
if (!dryRun) {
|
|
2351
|
+
await rm(artifactsDir, { recursive: true, force: true });
|
|
2352
|
+
}
|
|
2353
|
+
console.log(JSON.stringify({
|
|
2354
|
+
artifacts_dir: artifactsDir,
|
|
2355
|
+
action: dryRun ? "dry-run" : "deleted",
|
|
2356
|
+
status: status ?? "unknown",
|
|
2357
|
+
dry_run: dryRun,
|
|
2358
|
+
}, null, 2));
|
|
2359
|
+
}
|
|
2356
2360
|
async function cmdMcp(argv) {
|
|
2357
2361
|
await runAuditCodeMcpServer(argv.slice(3));
|
|
2358
2362
|
}
|
|
@@ -2401,6 +2405,9 @@ async function main(argv) {
|
|
|
2401
2405
|
case "synthesize":
|
|
2402
2406
|
await cmdSynthesize(argv);
|
|
2403
2407
|
return;
|
|
2408
|
+
case "cleanup":
|
|
2409
|
+
await cmdCleanup(argv);
|
|
2410
|
+
return;
|
|
2404
2411
|
case "mcp":
|
|
2405
2412
|
await cmdMcp(argv);
|
|
2406
2413
|
return;
|
|
@@ -2418,7 +2425,7 @@ async function main(argv) {
|
|
|
2418
2425
|
return;
|
|
2419
2426
|
default:
|
|
2420
2427
|
console.error(`Unknown command: ${command}`);
|
|
2421
|
-
console.error("Available commands: sample-run, advance-audit, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result");
|
|
2428
|
+
console.error("Available commands: sample-run, advance-audit, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result");
|
|
2422
2429
|
process.exitCode = 1;
|
|
2423
2430
|
}
|
|
2424
2431
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Lens, RepoManifest } from "../types.js";
|
|
2
|
+
import type { FileDisposition } from "../types/disposition.js";
|
|
3
|
+
import type { GraphBundle, GraphEdge } from "../types/graph.js";
|
|
4
|
+
import type { SurfaceRecord } from "../types/surfaces.js";
|
|
5
|
+
export declare const BROWSER_EXTENSION_HEURISTIC_NOTE = "Chrome extension manifest and HTML asset references were resolved deterministically from local paths; verify unusual dynamic registration manually.";
|
|
6
|
+
export declare function isBrowserExtensionManifestPath(path: string): boolean;
|
|
7
|
+
export declare function extractChromeExtensionManifestEdges(fromPath: string, content: string, pathLookup: Map<string, string>): GraphEdge[];
|
|
8
|
+
export declare function extractHtmlResourceEdges(fromPath: string, content: string, pathLookup: Map<string, string>): GraphEdge[];
|
|
9
|
+
export declare function hasBrowserExtensionManifestFile(repoManifest: RepoManifest): boolean;
|
|
10
|
+
export declare function deriveBrowserExtensionLensesForPath(path: string): Lens[];
|
|
11
|
+
export declare function inferBrowserExtensionUnitKind(path: string): string | undefined;
|
|
12
|
+
export declare function buildBrowserExtensionSurfacesFromGraph(graphBundle: GraphBundle | undefined, disposition?: FileDisposition): SurfaceRecord[];
|
|
13
|
+
export declare function chromeExtensionRiskSignalsForManifest(content: string): string[];
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { isAuditExcludedStatus } from "./disposition.js";
|
|
3
|
+
import { graphEdge, normalizeGraphPath, resolveCandidate, } from "./graphPathUtils.js";
|
|
4
|
+
export const BROWSER_EXTENSION_HEURISTIC_NOTE = "Chrome extension manifest and HTML asset references were resolved deterministically from local paths; verify unusual dynamic registration manually.";
|
|
5
|
+
const CHROME_EXTENSION_BACKGROUND_EDGE = "chrome-extension-background-link";
|
|
6
|
+
const CHROME_EXTENSION_CONTENT_SCRIPT_EDGE = "chrome-extension-content-script-link";
|
|
7
|
+
const CHROME_EXTENSION_CONTENT_STYLE_EDGE = "chrome-extension-content-style-link";
|
|
8
|
+
const CHROME_EXTENSION_UI_PAGE_EDGE = "chrome-extension-ui-page-link";
|
|
9
|
+
const CHROME_EXTENSION_WEB_ACCESSIBLE_EDGE = "chrome-extension-web-accessible-resource-link";
|
|
10
|
+
const HTML_RESOURCE_EDGE = "html-resource-link";
|
|
11
|
+
const CHROME_EXTENSION_EDGE_CONFIDENCE = 0.94;
|
|
12
|
+
const HTML_RESOURCE_EDGE_CONFIDENCE = 0.86;
|
|
13
|
+
const EXTENSION_SURFACE_EDGE_KINDS = new Set([
|
|
14
|
+
CHROME_EXTENSION_BACKGROUND_EDGE,
|
|
15
|
+
CHROME_EXTENSION_CONTENT_SCRIPT_EDGE,
|
|
16
|
+
CHROME_EXTENSION_UI_PAGE_EDGE,
|
|
17
|
+
]);
|
|
18
|
+
const HIGH_RISK_PERMISSION_TOKENS = [
|
|
19
|
+
"<all_urls>",
|
|
20
|
+
"activeTab",
|
|
21
|
+
"debugger",
|
|
22
|
+
"declarativeNetRequest",
|
|
23
|
+
"downloads",
|
|
24
|
+
"downloads.open",
|
|
25
|
+
"nativeMessaging",
|
|
26
|
+
"proxy",
|
|
27
|
+
"scripting",
|
|
28
|
+
"tabs",
|
|
29
|
+
"unlimitedStorage",
|
|
30
|
+
"webNavigation",
|
|
31
|
+
"webRequest",
|
|
32
|
+
];
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function asString(value) {
|
|
37
|
+
return typeof value === "string" && value.trim().length > 0
|
|
38
|
+
? value.trim()
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
function asStringArray(value) {
|
|
42
|
+
return Array.isArray(value)
|
|
43
|
+
? value
|
|
44
|
+
.map((item) => asString(item))
|
|
45
|
+
.filter((item) => item !== undefined)
|
|
46
|
+
: [];
|
|
47
|
+
}
|
|
48
|
+
function parseJsonObject(content) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(content);
|
|
51
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function isBrowserExtensionManifestPath(path) {
|
|
58
|
+
return posix.basename(normalizeGraphPath(path)).toLowerCase() === "manifest.json";
|
|
59
|
+
}
|
|
60
|
+
function isBrowserExtensionManifest(value) {
|
|
61
|
+
return (typeof value.manifest_version === "number" &&
|
|
62
|
+
(isRecord(value.background) ||
|
|
63
|
+
Array.isArray(value.content_scripts) ||
|
|
64
|
+
isRecord(value.action) ||
|
|
65
|
+
isRecord(value.browser_action) ||
|
|
66
|
+
isRecord(value.page_action) ||
|
|
67
|
+
isRecord(value.side_panel) ||
|
|
68
|
+
isRecord(value.options_ui) ||
|
|
69
|
+
typeof value.options_page === "string" ||
|
|
70
|
+
typeof value.devtools_page === "string" ||
|
|
71
|
+
isRecord(value.chrome_url_overrides) ||
|
|
72
|
+
Array.isArray(value.web_accessible_resources)));
|
|
73
|
+
}
|
|
74
|
+
function localPathCandidate(specifier) {
|
|
75
|
+
const withoutQuery = specifier.trim().split(/[?#]/, 1)[0]?.trim() ?? "";
|
|
76
|
+
if (withoutQuery.length === 0 ||
|
|
77
|
+
withoutQuery.startsWith("<") ||
|
|
78
|
+
withoutQuery.includes("*") ||
|
|
79
|
+
/^[a-z][a-z0-9+.-]*:/i.test(withoutQuery) ||
|
|
80
|
+
withoutQuery.startsWith("//")) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return normalizeGraphPath(withoutQuery).replace(/^\/+/, "");
|
|
84
|
+
}
|
|
85
|
+
function resolveLocalReference(fromPath, specifier, pathLookup) {
|
|
86
|
+
const local = localPathCandidate(specifier);
|
|
87
|
+
if (!local) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const isRootRelative = specifier.trim().startsWith("/");
|
|
91
|
+
const baseDir = posix.dirname(normalizeGraphPath(fromPath));
|
|
92
|
+
const candidate = isRootRelative || baseDir === "." ? local : posix.join(baseDir, local);
|
|
93
|
+
return resolveCandidate(candidate, pathLookup);
|
|
94
|
+
}
|
|
95
|
+
function addManifestReference(edges, params) {
|
|
96
|
+
const target = resolveLocalReference(params.fromPath, params.specifier, params.pathLookup);
|
|
97
|
+
if (!target) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
edges.push(graphEdge({
|
|
101
|
+
from: params.fromPath,
|
|
102
|
+
to: target,
|
|
103
|
+
kind: params.kind,
|
|
104
|
+
confidence: CHROME_EXTENSION_EDGE_CONFIDENCE,
|
|
105
|
+
reason: `Chrome extension manifest field '${params.field}' references '${params.specifier}'.`,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
function collectExtensionUiPageReferences(manifest) {
|
|
109
|
+
const entries = [];
|
|
110
|
+
for (const objectField of ["action", "browser_action", "page_action"]) {
|
|
111
|
+
const value = manifest[objectField];
|
|
112
|
+
if (isRecord(value)) {
|
|
113
|
+
const popup = asString(value.default_popup);
|
|
114
|
+
if (popup)
|
|
115
|
+
entries.push({ field: `${objectField}.default_popup`, specifier: popup });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const sidePanel = manifest.side_panel;
|
|
119
|
+
if (isRecord(sidePanel)) {
|
|
120
|
+
const path = asString(sidePanel.default_path);
|
|
121
|
+
if (path)
|
|
122
|
+
entries.push({ field: "side_panel.default_path", specifier: path });
|
|
123
|
+
}
|
|
124
|
+
const optionsPage = asString(manifest.options_page);
|
|
125
|
+
if (optionsPage)
|
|
126
|
+
entries.push({ field: "options_page", specifier: optionsPage });
|
|
127
|
+
const optionsUi = manifest.options_ui;
|
|
128
|
+
if (isRecord(optionsUi)) {
|
|
129
|
+
const page = asString(optionsUi.page);
|
|
130
|
+
if (page)
|
|
131
|
+
entries.push({ field: "options_ui.page", specifier: page });
|
|
132
|
+
}
|
|
133
|
+
const devtoolsPage = asString(manifest.devtools_page);
|
|
134
|
+
if (devtoolsPage)
|
|
135
|
+
entries.push({ field: "devtools_page", specifier: devtoolsPage });
|
|
136
|
+
const overrides = manifest.chrome_url_overrides;
|
|
137
|
+
if (isRecord(overrides)) {
|
|
138
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
139
|
+
const page = asString(value);
|
|
140
|
+
if (page)
|
|
141
|
+
entries.push({ field: `chrome_url_overrides.${key}`, specifier: page });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const sandbox = manifest.sandbox;
|
|
145
|
+
if (isRecord(sandbox)) {
|
|
146
|
+
asStringArray(sandbox.pages).forEach((page, index) => entries.push({ field: `sandbox.pages.${index}`, specifier: page }));
|
|
147
|
+
}
|
|
148
|
+
return entries;
|
|
149
|
+
}
|
|
150
|
+
function collectWebAccessibleReferences(manifest) {
|
|
151
|
+
const entries = [];
|
|
152
|
+
const resources = manifest.web_accessible_resources;
|
|
153
|
+
if (!Array.isArray(resources)) {
|
|
154
|
+
return entries;
|
|
155
|
+
}
|
|
156
|
+
resources.forEach((item, index) => {
|
|
157
|
+
if (typeof item === "string") {
|
|
158
|
+
entries.push({
|
|
159
|
+
field: `web_accessible_resources.${index}`,
|
|
160
|
+
specifier: item,
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!isRecord(item)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
asStringArray(item.resources).forEach((resource, resourceIndex) => entries.push({
|
|
168
|
+
field: `web_accessible_resources.${index}.resources.${resourceIndex}`,
|
|
169
|
+
specifier: resource,
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
172
|
+
return entries;
|
|
173
|
+
}
|
|
174
|
+
export function extractChromeExtensionManifestEdges(fromPath, content, pathLookup) {
|
|
175
|
+
if (!isBrowserExtensionManifestPath(fromPath)) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
const manifest = parseJsonObject(content);
|
|
179
|
+
if (!manifest || !isBrowserExtensionManifest(manifest)) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const edges = [];
|
|
183
|
+
const background = manifest.background;
|
|
184
|
+
if (isRecord(background)) {
|
|
185
|
+
const serviceWorker = asString(background.service_worker);
|
|
186
|
+
if (serviceWorker) {
|
|
187
|
+
addManifestReference(edges, {
|
|
188
|
+
fromPath,
|
|
189
|
+
field: "background.service_worker",
|
|
190
|
+
kind: CHROME_EXTENSION_BACKGROUND_EDGE,
|
|
191
|
+
specifier: serviceWorker,
|
|
192
|
+
pathLookup,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
asStringArray(background.scripts).forEach((script, index) => addManifestReference(edges, {
|
|
196
|
+
fromPath,
|
|
197
|
+
field: `background.scripts.${index}`,
|
|
198
|
+
kind: CHROME_EXTENSION_BACKGROUND_EDGE,
|
|
199
|
+
specifier: script,
|
|
200
|
+
pathLookup,
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
const contentScripts = manifest.content_scripts;
|
|
204
|
+
if (Array.isArray(contentScripts)) {
|
|
205
|
+
contentScripts.forEach((item, index) => {
|
|
206
|
+
if (!isRecord(item)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
asStringArray(item.js).forEach((script, scriptIndex) => addManifestReference(edges, {
|
|
210
|
+
fromPath,
|
|
211
|
+
field: `content_scripts.${index}.js.${scriptIndex}`,
|
|
212
|
+
kind: CHROME_EXTENSION_CONTENT_SCRIPT_EDGE,
|
|
213
|
+
specifier: script,
|
|
214
|
+
pathLookup,
|
|
215
|
+
}));
|
|
216
|
+
asStringArray(item.css).forEach((style, styleIndex) => addManifestReference(edges, {
|
|
217
|
+
fromPath,
|
|
218
|
+
field: `content_scripts.${index}.css.${styleIndex}`,
|
|
219
|
+
kind: CHROME_EXTENSION_CONTENT_STYLE_EDGE,
|
|
220
|
+
specifier: style,
|
|
221
|
+
pathLookup,
|
|
222
|
+
}));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
for (const { field, specifier } of collectExtensionUiPageReferences(manifest)) {
|
|
226
|
+
addManifestReference(edges, {
|
|
227
|
+
fromPath,
|
|
228
|
+
field,
|
|
229
|
+
kind: CHROME_EXTENSION_UI_PAGE_EDGE,
|
|
230
|
+
specifier,
|
|
231
|
+
pathLookup,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
for (const { field, specifier } of collectWebAccessibleReferences(manifest)) {
|
|
235
|
+
addManifestReference(edges, {
|
|
236
|
+
fromPath,
|
|
237
|
+
field,
|
|
238
|
+
kind: CHROME_EXTENSION_WEB_ACCESSIBLE_EDGE,
|
|
239
|
+
specifier,
|
|
240
|
+
pathLookup,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return edges;
|
|
244
|
+
}
|
|
245
|
+
function extractHtmlAttributeReferences(content, elementName, attributeName) {
|
|
246
|
+
const unquotedAttributeValue = "[^\\s\"'<>`]+";
|
|
247
|
+
const pattern = new RegExp(`<${elementName}\\b[^>]*\\b${attributeName}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|(${unquotedAttributeValue}))`, "gi");
|
|
248
|
+
const values = [];
|
|
249
|
+
for (const match of content.matchAll(pattern)) {
|
|
250
|
+
const value = match[1] ?? match[2] ?? match[3];
|
|
251
|
+
if (value)
|
|
252
|
+
values.push(value);
|
|
253
|
+
}
|
|
254
|
+
return values;
|
|
255
|
+
}
|
|
256
|
+
export function extractHtmlResourceEdges(fromPath, content, pathLookup) {
|
|
257
|
+
const normalized = normalizeGraphPath(fromPath).toLowerCase();
|
|
258
|
+
if (!normalized.endsWith(".html") && !normalized.endsWith(".htm")) {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
const references = [
|
|
262
|
+
...extractHtmlAttributeReferences(content, "script", "src"),
|
|
263
|
+
...extractHtmlAttributeReferences(content, "link", "href"),
|
|
264
|
+
];
|
|
265
|
+
const edges = [];
|
|
266
|
+
for (const specifier of references) {
|
|
267
|
+
const target = resolveLocalReference(fromPath, specifier, pathLookup);
|
|
268
|
+
if (!target) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
edges.push(graphEdge({
|
|
272
|
+
from: fromPath,
|
|
273
|
+
to: target,
|
|
274
|
+
kind: HTML_RESOURCE_EDGE,
|
|
275
|
+
confidence: HTML_RESOURCE_EDGE_CONFIDENCE,
|
|
276
|
+
reason: `HTML resource attribute references '${specifier}'.`,
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
return edges;
|
|
280
|
+
}
|
|
281
|
+
export function hasBrowserExtensionManifestFile(repoManifest) {
|
|
282
|
+
return repoManifest.files.some((file) => normalizeGraphPath(file.path).toLowerCase() === "manifest.json");
|
|
283
|
+
}
|
|
284
|
+
export function deriveBrowserExtensionLensesForPath(path) {
|
|
285
|
+
const normalized = normalizeGraphPath(path).toLowerCase();
|
|
286
|
+
if (normalized === "manifest.json") {
|
|
287
|
+
return ["security", "correctness", "config_deployment", "operability"];
|
|
288
|
+
}
|
|
289
|
+
if (normalized.startsWith("service/") ||
|
|
290
|
+
normalized.startsWith("background/") ||
|
|
291
|
+
normalized.includes("service-worker") ||
|
|
292
|
+
normalized.includes("background")) {
|
|
293
|
+
return ["security", "correctness", "reliability", "observability"];
|
|
294
|
+
}
|
|
295
|
+
if (normalized.startsWith("content/") || normalized.includes("content-script")) {
|
|
296
|
+
return ["security", "correctness", "reliability"];
|
|
297
|
+
}
|
|
298
|
+
if (normalized.includes("popup") ||
|
|
299
|
+
normalized.includes("sidebar") ||
|
|
300
|
+
normalized.includes("side-panel") ||
|
|
301
|
+
normalized.includes("panel") ||
|
|
302
|
+
normalized.endsWith(".html")) {
|
|
303
|
+
return ["security", "correctness", "maintainability"];
|
|
304
|
+
}
|
|
305
|
+
if (normalized.includes("worker")) {
|
|
306
|
+
return ["correctness", "reliability", "performance"];
|
|
307
|
+
}
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
export function inferBrowserExtensionUnitKind(path) {
|
|
311
|
+
const normalized = normalizeGraphPath(path).toLowerCase();
|
|
312
|
+
if (normalized === "manifest.json")
|
|
313
|
+
return "extension_config";
|
|
314
|
+
if (normalized.startsWith("service/") || normalized.startsWith("background/")) {
|
|
315
|
+
return "extension_background";
|
|
316
|
+
}
|
|
317
|
+
if (normalized.startsWith("content/"))
|
|
318
|
+
return "extension_content";
|
|
319
|
+
if (normalized.includes("worker"))
|
|
320
|
+
return "worker";
|
|
321
|
+
if (normalized.endsWith(".html"))
|
|
322
|
+
return "extension_ui";
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
function isExecutableExtensionSurfaceTarget(path) {
|
|
326
|
+
const normalized = normalizeGraphPath(path).toLowerCase();
|
|
327
|
+
return (normalized.endsWith(".js") ||
|
|
328
|
+
normalized.endsWith(".mjs") ||
|
|
329
|
+
normalized.endsWith(".cjs") ||
|
|
330
|
+
normalized.endsWith(".html") ||
|
|
331
|
+
normalized.endsWith(".htm"));
|
|
332
|
+
}
|
|
333
|
+
export function buildBrowserExtensionSurfacesFromGraph(graphBundle, disposition) {
|
|
334
|
+
const references = Array.isArray(graphBundle?.graphs.references)
|
|
335
|
+
? graphBundle.graphs.references
|
|
336
|
+
: [];
|
|
337
|
+
const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
|
|
338
|
+
const surfaces = [];
|
|
339
|
+
const seen = new Set();
|
|
340
|
+
for (const edge of references) {
|
|
341
|
+
if (!edge.kind || !EXTENSION_SURFACE_EDGE_KINDS.has(edge.kind)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (!isExecutableExtensionSurfaceTarget(edge.to)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const status = dispositionMap.get(edge.to);
|
|
348
|
+
if (status && isAuditExcludedStatus(status)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const kind = edge.kind === CHROME_EXTENSION_BACKGROUND_EDGE ? "background" : "interface";
|
|
352
|
+
const key = `${kind}:${edge.to}`;
|
|
353
|
+
if (seen.has(key)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
seen.add(key);
|
|
357
|
+
surfaces.push({
|
|
358
|
+
id: `surface:${edge.to}`,
|
|
359
|
+
kind,
|
|
360
|
+
entrypoint: edge.to,
|
|
361
|
+
exposure: edge.kind === CHROME_EXTENSION_CONTENT_SCRIPT_EDGE ? "network" : "local",
|
|
362
|
+
notes: [BROWSER_EXTENSION_HEURISTIC_NOTE],
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return surfaces.sort((a, b) => a.entrypoint.localeCompare(b.entrypoint) || a.kind.localeCompare(b.kind));
|
|
366
|
+
}
|
|
367
|
+
export function chromeExtensionRiskSignalsForManifest(content) {
|
|
368
|
+
const manifest = parseJsonObject(content);
|
|
369
|
+
if (!manifest || !isBrowserExtensionManifest(manifest)) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
const permissions = [
|
|
373
|
+
...asStringArray(manifest.permissions),
|
|
374
|
+
...asStringArray(manifest.optional_permissions),
|
|
375
|
+
...asStringArray(manifest.host_permissions),
|
|
376
|
+
];
|
|
377
|
+
return HIGH_RISK_PERMISSION_TOKENS.filter((token) => permissions.some((permission) => permission === token));
|
|
378
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isNodeModulesOrGit, isBuildOutput, isVendorPath, isBinaryArtifact, isLicensePath, isLockfilePath, isLogPath, isDocPath, isAuditArtifactPath, isGeneratedTestArtifactPath, isGeneratedInstallArtifactPath, isExamplesOrFixturesPath, normalizeExtractorPath, } from "./pathPatterns.js";
|
|
1
|
+
import { isNodeModulesOrGit, isBuildOutput, isVendorPath, isBinaryArtifact, isLicensePath, isLockfilePath, isLogPath, isDocPath, isGeneratedPath, isAuditArtifactPath, isGeneratedTestArtifactPath, isGeneratedInstallArtifactPath, isExamplesOrFixturesPath, normalizeExtractorPath, } from "./pathPatterns.js";
|
|
2
2
|
function inferDisposition(path) {
|
|
3
3
|
const normalized = normalizeExtractorPath(path);
|
|
4
4
|
if (isNodeModulesOrGit(normalized)) {
|
|
@@ -33,6 +33,13 @@ function inferDisposition(path) {
|
|
|
33
33
|
reason: "Generated audit artifact.",
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
if (isGeneratedPath(normalized)) {
|
|
37
|
+
return {
|
|
38
|
+
path,
|
|
39
|
+
status: "generated",
|
|
40
|
+
reason: "Generated artifact path.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
36
43
|
if (isGeneratedTestArtifactPath(normalized)) {
|
|
37
44
|
return {
|
|
38
45
|
path,
|
package/dist/extractors/graph.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { posix } from "node:path";
|
|
4
4
|
import { isAuditExcludedStatus } from "./disposition.js";
|
|
5
|
+
import { extractChromeExtensionManifestEdges, extractHtmlResourceEdges, } from "./browserExtension.js";
|
|
5
6
|
import { extractCargoWorkspaceMemberEdges, extractGoWorkspaceModuleEdges, extractMavenModuleEdges, extractPackageEntrypointEdges, extractPackageScriptEdges, extractPyprojectTestpathLinks, extractTypescriptProjectReferenceEdges, extractWorkspacePackageEdges, extractYamlPathReferenceEdges, isCargoManifestPath, isGoWorkspaceManifestPath, isMavenPomPath, isPyprojectPath, } from "./graphManifestEdges.js";
|
|
6
7
|
import { graphEdge, graphLookupKey, normalizeGraphPath, resolveCandidate, } from "./graphPathUtils.js";
|
|
7
8
|
import { isTestPath, normalizeExtractorPath } from "./pathPatterns.js";
|
|
@@ -10,6 +11,7 @@ const SOURCE_LANGUAGES = new Set([
|
|
|
10
11
|
"typescript",
|
|
11
12
|
"javascript",
|
|
12
13
|
"json",
|
|
14
|
+
"html",
|
|
13
15
|
"yaml",
|
|
14
16
|
"python",
|
|
15
17
|
"go",
|
|
@@ -27,6 +29,8 @@ const SOURCE_EXTENSIONS = [
|
|
|
27
29
|
".mjs",
|
|
28
30
|
".cjs",
|
|
29
31
|
".json",
|
|
32
|
+
".html",
|
|
33
|
+
".htm",
|
|
30
34
|
".yml",
|
|
31
35
|
".yaml",
|
|
32
36
|
".py",
|
|
@@ -1309,6 +1313,8 @@ export function buildGraphBundle(repoManifest, disposition, options = {}) {
|
|
|
1309
1313
|
references.push(...extractReferenceEdges(file.path, content, pathLookup));
|
|
1310
1314
|
references.push(...extractJsonSchemaReferenceEdges(file.path, content, pathLookup));
|
|
1311
1315
|
references.push(...extractPackageEntrypointEdges(file.path, content, pathLookup));
|
|
1316
|
+
references.push(...extractChromeExtensionManifestEdges(file.path, content, pathLookup));
|
|
1317
|
+
references.push(...extractHtmlResourceEdges(file.path, content, pathLookup));
|
|
1312
1318
|
references.push(...extractPackageScriptEdges(file.path, content, pathLookup));
|
|
1313
1319
|
references.push(...extractWorkspacePackageEdges(file.path, content, pathLookup));
|
|
1314
1320
|
references.push(...extractTypescriptProjectReferenceEdges(file.path, content, pathLookup));
|
|
@@ -10,6 +10,16 @@ const BINARY_EXTENSIONS = [
|
|
|
10
10
|
".jpg",
|
|
11
11
|
".jpeg",
|
|
12
12
|
".gif",
|
|
13
|
+
".webp",
|
|
14
|
+
".svg",
|
|
15
|
+
".ico",
|
|
16
|
+
".bmp",
|
|
17
|
+
".avif",
|
|
18
|
+
".wasm",
|
|
19
|
+
".woff",
|
|
20
|
+
".woff2",
|
|
21
|
+
".ttf",
|
|
22
|
+
".otf",
|
|
13
23
|
".pdf",
|
|
14
24
|
".zip",
|
|
15
25
|
];
|
|
@@ -216,7 +226,11 @@ export function isDeploymentConfigPath(normalized) {
|
|
|
216
226
|
return hasToken(normalized, DEPLOYMENT_KEYWORDS) || endsWithAny(normalized, [".yml", ".yaml"]);
|
|
217
227
|
}
|
|
218
228
|
export function isGeneratedPath(normalized) {
|
|
219
|
-
return isVendorPath(normalized) ||
|
|
229
|
+
return (isVendorPath(normalized) ||
|
|
230
|
+
normalized.endsWith(".map") ||
|
|
231
|
+
normalized.endsWith(".wasm.mjs") ||
|
|
232
|
+
normalized.endsWith(".wasm.js") ||
|
|
233
|
+
hasToken(normalized, ["generated", "autogenerated"]));
|
|
220
234
|
}
|
|
221
235
|
export function isSurfacePath(normalized) {
|
|
222
236
|
return (hasSegment(normalized, "api") ||
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { RepoManifest } from "../types.js";
|
|
2
2
|
import type { FileDisposition } from "../types/disposition.js";
|
|
3
|
+
import type { GraphBundle } from "../types/graph.js";
|
|
3
4
|
import type { SurfaceManifest } from "../types/surfaces.js";
|
|
4
5
|
/**
|
|
5
6
|
* Detects likely execution surfaces from file paths using the shared extractor
|
|
6
7
|
* heuristics, primarily to seed later audit planning.
|
|
7
8
|
*/
|
|
8
|
-
export declare function buildSurfaceManifest(repoManifest: RepoManifest, disposition?: FileDisposition
|
|
9
|
+
export declare function buildSurfaceManifest(repoManifest: RepoManifest, disposition?: FileDisposition, options?: {
|
|
10
|
+
graphBundle?: GraphBundle;
|
|
11
|
+
}): SurfaceManifest;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildBrowserExtensionSurfacesFromGraph } from "./browserExtension.js";
|
|
1
2
|
import { isAuditExcludedStatus } from "./disposition.js";
|
|
2
3
|
import { EXTRACTOR_HEURISTIC_NOTE, isBackgroundSurfacePath, isNetworkSurfacePath, isSurfacePath, normalizeExtractorPath, } from "./pathPatterns.js";
|
|
3
4
|
function methodsForPath(path) {
|
|
@@ -11,9 +12,18 @@ function methodsForPath(path) {
|
|
|
11
12
|
* Detects likely execution surfaces from file paths using the shared extractor
|
|
12
13
|
* heuristics, primarily to seed later audit planning.
|
|
13
14
|
*/
|
|
14
|
-
export function buildSurfaceManifest(repoManifest, disposition) {
|
|
15
|
+
export function buildSurfaceManifest(repoManifest, disposition, options = {}) {
|
|
15
16
|
const surfaces = [];
|
|
17
|
+
const seen = new Set();
|
|
16
18
|
const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
|
|
19
|
+
function addSurface(surface) {
|
|
20
|
+
const key = `${surface.kind}:${surface.entrypoint}`;
|
|
21
|
+
if (seen.has(key)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
seen.add(key);
|
|
25
|
+
surfaces.push(surface);
|
|
26
|
+
}
|
|
17
27
|
for (const file of repoManifest.files) {
|
|
18
28
|
const status = dispositionMap.get(file.path);
|
|
19
29
|
if (status && isAuditExcludedStatus(status)) {
|
|
@@ -21,7 +31,7 @@ export function buildSurfaceManifest(repoManifest, disposition) {
|
|
|
21
31
|
}
|
|
22
32
|
const normalized = normalizeExtractorPath(file.path);
|
|
23
33
|
if (isSurfacePath(normalized)) {
|
|
24
|
-
|
|
34
|
+
addSurface({
|
|
25
35
|
id: `surface:${file.path}`,
|
|
26
36
|
kind: isBackgroundSurfacePath(normalized) ? "background" : "interface",
|
|
27
37
|
entrypoint: file.path,
|
|
@@ -31,5 +41,8 @@ export function buildSurfaceManifest(repoManifest, disposition) {
|
|
|
31
41
|
});
|
|
32
42
|
}
|
|
33
43
|
}
|
|
44
|
+
for (const surface of buildBrowserExtensionSurfacesFromGraph(options.graphBundle, disposition)) {
|
|
45
|
+
addSurface(surface);
|
|
46
|
+
}
|
|
34
47
|
return { surfaces };
|
|
35
48
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { isAuditExcludedStatus } from "../extractors/disposition.js";
|
|
3
4
|
import { resolveNodeTool, runFirstAvailableCommand, } from "./localCommands.js";
|
|
@@ -5,6 +6,35 @@ function tryRunConfiguredFormatter(root, candidates) {
|
|
|
5
6
|
const result = runFirstAvailableCommand(root, candidates);
|
|
6
7
|
return result !== null && !result.error && result.exitCode === 0;
|
|
7
8
|
}
|
|
9
|
+
const PRETTIER_CONFIG_FILES = [
|
|
10
|
+
".prettierrc",
|
|
11
|
+
".prettierrc.json",
|
|
12
|
+
".prettierrc.yml",
|
|
13
|
+
".prettierrc.yaml",
|
|
14
|
+
".prettierrc.json5",
|
|
15
|
+
".prettierrc.js",
|
|
16
|
+
".prettierrc.cjs",
|
|
17
|
+
".prettierrc.mjs",
|
|
18
|
+
"prettier.config.js",
|
|
19
|
+
"prettier.config.cjs",
|
|
20
|
+
"prettier.config.mjs",
|
|
21
|
+
];
|
|
22
|
+
function hasPrettierConfig(root) {
|
|
23
|
+
if (PRETTIER_CONFIG_FILES.some((file) => existsSync(join(root, file)))) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const packageJsonPath = join(root, "package.json");
|
|
27
|
+
if (!existsSync(packageJsonPath)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
32
|
+
return packageJson.prettier !== undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
8
38
|
export function runAutoFixExecutor(bundle, root) {
|
|
9
39
|
if (!bundle.file_disposition) {
|
|
10
40
|
throw new Error("Cannot run auto fix executor without file_disposition");
|
|
@@ -20,16 +50,17 @@ export function runAutoFixExecutor(bundle, root) {
|
|
|
20
50
|
}
|
|
21
51
|
const executedTools = [];
|
|
22
52
|
// JS, TS, HTML, CSS, JSON, YAML, MD
|
|
23
|
-
if (
|
|
24
|
-
extensions.has("
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
if (hasPrettierConfig(root) &&
|
|
54
|
+
(extensions.has("ts") ||
|
|
55
|
+
extensions.has("js") ||
|
|
56
|
+
extensions.has("tsx") ||
|
|
57
|
+
extensions.has("jsx") ||
|
|
58
|
+
extensions.has("html") ||
|
|
59
|
+
extensions.has("css") ||
|
|
60
|
+
extensions.has("json") ||
|
|
61
|
+
extensions.has("yml") ||
|
|
62
|
+
extensions.has("yaml") ||
|
|
63
|
+
extensions.has("md"))) {
|
|
33
64
|
if (tryRunConfiguredFormatter(root, [
|
|
34
65
|
...resolveNodeTool(root, join("node_modules", "prettier", "bin", "prettier.cjs"), ["--write", "."], "prettier --write ."),
|
|
35
66
|
{ command: "prettier", args: ["--write", "."], display: "prettier --write ." },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { buildFileDisposition } from "../extractors/disposition.js";
|
|
2
|
+
import { buildFileDisposition, isAuditExcludedStatus, } from "../extractors/disposition.js";
|
|
3
3
|
import { buildGraphBundle, buildGraphBundleFromFs, } from "../extractors/graph.js";
|
|
4
4
|
import { buildCriticalFlowManifest } from "../extractors/flows.js";
|
|
5
5
|
import { buildRiskRegister } from "../extractors/risk.js";
|
|
@@ -123,6 +123,10 @@ export async function runIntakeExecutor(bundle, root) {
|
|
|
123
123
|
hash_files: false,
|
|
124
124
|
});
|
|
125
125
|
const disposition = buildFileDisposition(repoManifest);
|
|
126
|
+
const auditableCount = disposition.files.filter((file) => !isAuditExcludedStatus(file.status)).length;
|
|
127
|
+
if (auditableCount === 0) {
|
|
128
|
+
throw new Error(`No auditable files found in ${root}. The repository may be empty, generated-only, documentation-only, or filtered by .auditorignore.`);
|
|
129
|
+
}
|
|
126
130
|
return {
|
|
127
131
|
updated: {
|
|
128
132
|
...bundle,
|
|
@@ -140,7 +144,6 @@ export async function runStructureExecutor(bundle, root) {
|
|
|
140
144
|
const externalAnalyzerResults = bundle.external_analyzer_results;
|
|
141
145
|
const disposition = bundle.file_disposition ?? buildFileDisposition(bundle.repo_manifest);
|
|
142
146
|
const unitManifest = buildUnitManifest(bundle.repo_manifest, disposition);
|
|
143
|
-
const surfaceManifest = buildSurfaceManifest(bundle.repo_manifest, disposition);
|
|
144
147
|
const graphBundle = root
|
|
145
148
|
? await buildGraphBundleFromFs(bundle.repo_manifest, root, disposition, {
|
|
146
149
|
externalAnalyzerResults,
|
|
@@ -148,6 +151,7 @@ export async function runStructureExecutor(bundle, root) {
|
|
|
148
151
|
: buildGraphBundle(bundle.repo_manifest, disposition, {
|
|
149
152
|
externalAnalyzerResults,
|
|
150
153
|
});
|
|
154
|
+
const surfaceManifest = buildSurfaceManifest(bundle.repo_manifest, disposition, { graphBundle });
|
|
151
155
|
const criticalFlows = buildCriticalFlowManifest(bundle.repo_manifest, surfaceManifest, disposition);
|
|
152
156
|
const riskRegister = buildRiskRegister(unitManifest, criticalFlows, externalAnalyzerResults);
|
|
153
157
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyUnitCoverage, createCoverageMatrix, markExcludedPath, } from "../coverage.js";
|
|
2
2
|
import { isAuditExcludedStatus } from "../extractors/disposition.js";
|
|
3
|
+
import { hasBrowserExtensionManifestFile } from "../extractors/browserExtension.js";
|
|
3
4
|
import { deriveRequiredLensesForPath } from "./unitBuilder.js";
|
|
4
5
|
const CATEGORY_LENS_TABLE = [
|
|
5
6
|
[["security", "secret"], ["security", "correctness"]],
|
|
@@ -47,6 +48,7 @@ function applyAnalyzerCoverage(coverage, externalAnalyzerResults) {
|
|
|
47
48
|
}
|
|
48
49
|
export function initializeCoverageFromPlan(repoManifest, unitManifest, disposition, externalAnalyzerResults) {
|
|
49
50
|
const coverage = createCoverageMatrix(repoManifest.files.map((file) => file.path));
|
|
51
|
+
const isBrowserExtensionProject = hasBrowserExtensionManifestFile(repoManifest);
|
|
50
52
|
const dispositionMap = new Map(disposition.files.map((item) => [item.path, item.status]));
|
|
51
53
|
for (const file of repoManifest.files) {
|
|
52
54
|
const status = dispositionMap.get(file.path);
|
|
@@ -66,7 +68,9 @@ export function initializeCoverageFromPlan(repoManifest, unitManifest, dispositi
|
|
|
66
68
|
}
|
|
67
69
|
for (const file of repoManifest.files) {
|
|
68
70
|
const unitIds = unitIdsByPath.get(file.path) ?? [];
|
|
69
|
-
const requiredLenses = deriveRequiredLensesForPath(file.path
|
|
71
|
+
const requiredLenses = deriveRequiredLensesForPath(file.path, {
|
|
72
|
+
isBrowserExtensionProject,
|
|
73
|
+
});
|
|
70
74
|
for (const unitId of unitIds) {
|
|
71
75
|
applyUnitCoverage(coverage, file.path, unitId, requiredLenses);
|
|
72
76
|
}
|
|
@@ -15,6 +15,14 @@ const ESLINT_CONFIG_FILES = [
|
|
|
15
15
|
".eslintrc.yml",
|
|
16
16
|
".eslintrc.yaml",
|
|
17
17
|
];
|
|
18
|
+
const TSCONFIG_FILES = [
|
|
19
|
+
"tsconfig.json",
|
|
20
|
+
"tsconfig.build.json",
|
|
21
|
+
"jsconfig.json",
|
|
22
|
+
];
|
|
23
|
+
function hasTypeScriptConfig(root) {
|
|
24
|
+
return TSCONFIG_FILES.some((file) => existsSync(join(root, file)));
|
|
25
|
+
}
|
|
18
26
|
function hasEslintConfig(root) {
|
|
19
27
|
if (ESLINT_CONFIG_FILES.some((file) => existsSync(join(root, file)))) {
|
|
20
28
|
return true;
|
|
@@ -109,7 +117,8 @@ function runEslint(root) {
|
|
|
109
117
|
}
|
|
110
118
|
export function runSyntaxResolutionExecutor(bundle, root) {
|
|
111
119
|
const items = [];
|
|
112
|
-
if (
|
|
120
|
+
if (hasTypeScriptConfig(root) &&
|
|
121
|
+
bundle.file_disposition?.files.some((f) => f.path.endsWith(".ts"))) {
|
|
113
122
|
items.push(...runTsc(root));
|
|
114
123
|
}
|
|
115
124
|
if (bundle.file_disposition?.files.some((f) => f.path.endsWith(".ts") || f.path.endsWith(".js"))) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Lens, RepoManifest, UnitManifest } from "../types.js";
|
|
2
2
|
import type { FileDisposition } from "../types/disposition.js";
|
|
3
3
|
export declare const LENS_ORDER: Lens[];
|
|
4
|
-
export declare function deriveRequiredLensesForPath(path: string
|
|
4
|
+
export declare function deriveRequiredLensesForPath(path: string, options?: {
|
|
5
|
+
isBrowserExtensionProject?: boolean;
|
|
6
|
+
}): Lens[];
|
|
5
7
|
export declare function buildUnitManifest(repoManifest: RepoManifest, disposition?: FileDisposition): UnitManifest;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deriveBrowserExtensionLensesForPath, hasBrowserExtensionManifestFile, inferBrowserExtensionUnitKind, } from "../extractors/browserExtension.js";
|
|
1
2
|
import { bucketFile } from "../extractors/bucketing.js";
|
|
2
3
|
import { isAuditExcludedStatus } from "../extractors/disposition.js";
|
|
3
4
|
import { pathTokens, normalizeExtractorPath } from "../extractors/pathPatterns.js";
|
|
@@ -14,7 +15,13 @@ const LENS_MAP = {
|
|
|
14
15
|
generated_vendor: ["maintainability"],
|
|
15
16
|
unknown: ["correctness"],
|
|
16
17
|
};
|
|
17
|
-
function inferUnitKind(path) {
|
|
18
|
+
function inferUnitKind(path, isBrowserExtensionProject = false) {
|
|
19
|
+
if (isBrowserExtensionProject) {
|
|
20
|
+
const extensionKind = inferBrowserExtensionUnitKind(path);
|
|
21
|
+
if (extensionKind) {
|
|
22
|
+
return extensionKind;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
18
25
|
const normalized = path.toLowerCase();
|
|
19
26
|
if (normalized.startsWith("apps/") || normalized.startsWith("services/"))
|
|
20
27
|
return "service";
|
|
@@ -94,7 +101,7 @@ function applyExtensionLensGuards(path, lenses) {
|
|
|
94
101
|
}
|
|
95
102
|
return lenses;
|
|
96
103
|
}
|
|
97
|
-
export function deriveRequiredLensesForPath(path) {
|
|
104
|
+
export function deriveRequiredLensesForPath(path, options = {}) {
|
|
98
105
|
const assignment = bucketFile(path);
|
|
99
106
|
const required = new Set();
|
|
100
107
|
for (const bucket of assignment.buckets) {
|
|
@@ -102,6 +109,11 @@ export function deriveRequiredLensesForPath(path) {
|
|
|
102
109
|
required.add(lens);
|
|
103
110
|
}
|
|
104
111
|
}
|
|
112
|
+
if (options.isBrowserExtensionProject) {
|
|
113
|
+
for (const lens of deriveBrowserExtensionLensesForPath(path)) {
|
|
114
|
+
required.add(lens);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
105
117
|
return applyExtensionLensGuards(path, sortLenses(required));
|
|
106
118
|
}
|
|
107
119
|
function inferCriticalFlows(files, requiredLenses) {
|
|
@@ -128,13 +140,14 @@ function inferCriticalFlows(files, requiredLenses) {
|
|
|
128
140
|
}
|
|
129
141
|
export function buildUnitManifest(repoManifest, disposition) {
|
|
130
142
|
const units = new Map();
|
|
143
|
+
const isBrowserExtensionProject = hasBrowserExtensionManifestFile(repoManifest);
|
|
131
144
|
const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
|
|
132
145
|
for (const file of repoManifest.files) {
|
|
133
146
|
const status = dispositionMap.get(file.path);
|
|
134
147
|
if (file.excluded || (status && isAuditExcludedStatus(status))) {
|
|
135
148
|
continue;
|
|
136
149
|
}
|
|
137
|
-
const kind = inferUnitKind(file.path);
|
|
150
|
+
const kind = inferUnitKind(file.path, isBrowserExtensionProject);
|
|
138
151
|
const unitId = inferUnitId(file.path, kind);
|
|
139
152
|
const existing = units.get(unitId) ?? {
|
|
140
153
|
unit_id: unitId,
|
|
@@ -148,7 +161,9 @@ export function buildUnitManifest(repoManifest, disposition) {
|
|
|
148
161
|
}
|
|
149
162
|
const assignment = bucketFile(file.path);
|
|
150
163
|
const required = new Set(existing.required_lenses);
|
|
151
|
-
for (const lens of deriveRequiredLensesForPath(file.path
|
|
164
|
+
for (const lens of deriveRequiredLensesForPath(file.path, {
|
|
165
|
+
isBrowserExtensionProject,
|
|
166
|
+
})) {
|
|
152
167
|
required.add(lens);
|
|
153
168
|
}
|
|
154
169
|
existing.required_lenses = sortLenses(required);
|
|
@@ -118,7 +118,7 @@ function buildInteractiveProviderHint(status, providerName, sessionConfigPath, i
|
|
|
118
118
|
return null;
|
|
119
119
|
}
|
|
120
120
|
if (isConfigError) {
|
|
121
|
-
return `Configuration error: Verify --root points to
|
|
121
|
+
return `Configuration error: Verify --root points to the intended repository root and that the tree contains auditable files.`;
|
|
122
122
|
}
|
|
123
123
|
const providerLabel = providerName ?? LOCAL_SUBPROCESS_PROVIDER_NAME;
|
|
124
124
|
return `Provider: ${providerLabel}. For automatic LLM review, configure an interactive provider in ${sessionConfigPath}.`;
|
package/docs/operator-guide.md
CHANGED
|
@@ -105,12 +105,20 @@ audit-code --updates /path/to/runtime_validation_update.json
|
|
|
105
105
|
audit-code --external-analyzer-results /path/to/external_analyzer_results.json
|
|
106
106
|
audit-code explain-task <task_id>
|
|
107
107
|
audit-code validate
|
|
108
|
+
audit-code cleanup
|
|
108
109
|
audit-code mcp
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
`audit-code validate` checks artifact shape, cross-artifact consistency,
|
|
112
113
|
session config, and explicit provider readiness.
|
|
113
114
|
|
|
115
|
+
`audit-code cleanup` removes the `.audit-artifacts/` directory when safe to
|
|
116
|
+
do so. It reads `audit_state.json` before acting: `complete` and `not_started`
|
|
117
|
+
states are deleted unconditionally; `active` and `blocked` states are refused
|
|
118
|
+
(the audit is resumable). If `audit_state.json` is missing — typically a
|
|
119
|
+
crashed run — cleanup also refuses unless `--force` is passed. `--dry-run`
|
|
120
|
+
previews the action without deleting anything.
|
|
121
|
+
|
|
114
122
|
## Session config
|
|
115
123
|
|
|
116
124
|
Backend fallback configuration lives at:
|