auditor-lambda 0.3.18 → 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/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
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));
@@ -716,35 +692,6 @@ async function cmdRunToCompletion(argv) {
716
692
  let pendingBatchAuditResults = batchResultsDir
717
693
  ? await listBatchResultFiles(batchResultsDir)
718
694
  : [];
719
- const earlyBundle = await loadArtifactBundle(artifactsDir);
720
- if (!earlyBundle.unit_manifest) {
721
- const foundSignal = await detectProjectRoot(root);
722
- if (!foundSignal) {
723
- 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.`;
724
- const earlyState = deriveAuditState(earlyBundle);
725
- const blockedState = buildBlockedAuditState({
726
- state: earlyState,
727
- obligationId: null,
728
- executor: null,
729
- blocker,
730
- });
731
- await emitEnvelope({
732
- root,
733
- artifactsDir,
734
- bundle: { ...earlyBundle, audit_state: blockedState },
735
- audit_state: blockedState,
736
- selected_obligation: null,
737
- selected_executor: null,
738
- progress_made: false,
739
- artifacts_written: [],
740
- progress_summary: blocker,
741
- next_likely_step: null,
742
- providerName: provider.name,
743
- isConfigError: true,
744
- });
745
- return;
746
- }
747
- }
748
695
  let pendingAuditResultsPath = getFlag(argv, "--results");
749
696
  let pendingRuntimeUpdatesPath = getFlag(argv, "--updates");
750
697
  let pendingExternalAnalyzerPath = getFlag(argv, "--external-analyzer-results");
@@ -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,
@@ -11,6 +11,7 @@ const DEFAULT_IGNORES = [
11
11
  ".turbo",
12
12
  ".artifacts",
13
13
  ".audit-artifacts",
14
+ ".audit-code/install",
14
15
  ".agent",
15
16
  ".claude",
16
17
  "coverage",
@@ -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) || hasToken(normalized, ["generated", "autogenerated"]);
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): SurfaceManifest;
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
- surfaces.push({
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 (extensions.has("ts") ||
24
- extensions.has("js") ||
25
- extensions.has("tsx") ||
26
- extensions.has("jsx") ||
27
- extensions.has("html") ||
28
- extensions.has("css") ||
29
- extensions.has("json") ||
30
- extensions.has("yml") ||
31
- extensions.has("yaml") ||
32
- extensions.has("md")) {
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 (bundle.file_disposition?.files.some((f) => f.path.endsWith(".ts"))) {
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): Lens[];
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 a repository root (with package.json, go.mod, etc.).`;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",