weavatrix 0.2.13 → 0.2.15
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 +99 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.15.md +37 -0
- package/package.json +2 -2
- package/skill/SKILL.md +57 -38
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +14 -71
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/conventions.js +16 -0
- package/src/analysis/dependency/scoped-dependencies.js +45 -0
- package/src/analysis/dependency/source-references.js +21 -0
- package/src/analysis/duplicates.tokenize.js +12 -2
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.reach.js +20 -0
- package/src/analysis/internal-audit.run.js +114 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/analysis/task-retrieval.js +3 -14
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +20 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/git-output.mjs +10 -0
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +197 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/reverse-reach.mjs +42 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +97 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -331
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp/tools-impact-change.mjs +2 -38
- package/src/mcp/tools-impact.mjs +2 -43
- package/src/mcp-server.mjs +35 -146
- package/src/path-classification.js +14 -0
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/security/malware-heuristics.sweep.js +1 -1
- package/src/security/registry-sig.classify.js +2 -1
- package/src/security/registry-sig.rules.js +3 -3
- package/src/version.js +7 -0
- package/docs/releases/v0.2.13.md +0 -48
|
@@ -1,34 +1,39 @@
|
|
|
1
|
-
// internal-audit.run.js — the audit runner: loads a repo's graph.json + package.json, runs
|
|
2
|
-
// dead-check (files) + computeUnusedExports + dep-check, and emits the unified findings envelope
|
|
3
|
-
// (DEPS_SECURITY_PLAN.md §2.2-2.3). Split from internal-audit.js.
|
|
4
1
|
import { existsSync } from "node:fs";
|
|
5
|
-
import {
|
|
2
|
+
import { basename, join } from "node:path";
|
|
6
3
|
import { computeDead, computeUnusedExports } from "./dead-check.js";
|
|
7
|
-
import {
|
|
8
|
-
import { parseGoMod } from "./manifests.js";
|
|
4
|
+
import { computeGoDepFindings, computePyDepFindings, computeScopedDepFindings } from "./dep-check.js";
|
|
9
5
|
import { computeStructureFindings } from "./dep-rules.js";
|
|
10
|
-
import { makeFinding,
|
|
6
|
+
import { makeFinding, sortFindings, summarizeFindings } from "./findings.js";
|
|
11
7
|
import { graphOutDirForRepo } from "../graph/layout.js";
|
|
12
|
-
import { collectInstalled } from "../security/installed.js";
|
|
13
|
-
import { loadStore, queryStore, advisoryQueryFingerprint } from "../security/advisory-store.js";
|
|
14
|
-
import { matchAdvisories } from "../security/match.js";
|
|
15
|
-
import { scanMalware } from "../security/malware-heuristics.js";
|
|
16
|
-
import { classifyTyposquat } from "../security/typosquat.js";
|
|
17
8
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
TEST_FILE_RE,
|
|
10
|
+
collectConfigTexts,
|
|
11
|
+
collectNonRuntimeRoots,
|
|
12
|
+
collectPackageScopes,
|
|
13
|
+
collectPyManifest,
|
|
14
|
+
collectSourceTexts,
|
|
15
|
+
listRepoFiles,
|
|
16
|
+
readJson,
|
|
17
|
+
readRepoJson,
|
|
18
|
+
readRepoText,
|
|
19
|
+
workspacePkgNames,
|
|
20
20
|
} from "./internal-audit.collect.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import { computeReachability, entryFiles } from "./internal-audit.reach.js";
|
|
22
|
+
import { parseGoMod } from "./manifests.js";
|
|
23
23
|
import { PATH_CLASS_NAMES, createPathClassifier, hasPathClass } from "../path-classification.js";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
import { createRepoBoundary } from "../repo-path.js";
|
|
25
|
+
import { analyzeSourceCorrectness } from "./source-correctness.js";
|
|
26
|
+
import { collectJvmDependencyEvidence } from "./jvm-dependency-evidence.js";
|
|
27
|
+
import { buildDependencyHealth } from "./internal-audit/dependency-health.js";
|
|
28
|
+
import { runSupplyChainChecks } from "./internal-audit/supply-chain.js";
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
export async function runInternalAudit(repoPath, {
|
|
31
|
+
graph,
|
|
32
|
+
advisoryStorePath,
|
|
33
|
+
skipMalwareScan = false,
|
|
34
|
+
malwareExclusions = {},
|
|
35
|
+
rgPath = "",
|
|
36
|
+
} = {}) {
|
|
32
37
|
if (!existsSync(repoPath)) return { ok: false, error: "Repo path not found" };
|
|
33
38
|
const boundary = createRepoBoundary(repoPath);
|
|
34
39
|
if (!boundary.root) return { ok: false, error: "Repository path is unreadable" };
|
|
@@ -39,12 +44,12 @@ export async function runInternalAudit(repoPath, { graph, advisoryStorePath, ski
|
|
|
39
44
|
const pkg = readRepoJson(boundary, "package.json") || {};
|
|
40
45
|
const packageScopes = collectPackageScopes(repoPath, pkg);
|
|
41
46
|
const externalImports = graph.externalImports || [];
|
|
42
|
-
const dynamicTargets = new Set(externalImports.filter((
|
|
47
|
+
const dynamicTargets = new Set(externalImports.filter((entry) => entry.dynamic && entry.target).map((entry) => entry.target));
|
|
43
48
|
const rules = readRepoJson(boundary, ".weavatrix-deps.json") || {};
|
|
44
|
-
|
|
45
|
-
// Graphs can be stale or miss a helper file; text fallbacks must scan the real repo tree too.
|
|
49
|
+
const repoFiles = listRepoFiles(repoPath);
|
|
46
50
|
const sources = collectSourceTexts(repoPath, graph);
|
|
47
51
|
const nonRuntimeRoots = collectNonRuntimeRoots(repoPath, rules);
|
|
52
|
+
|
|
48
53
|
const pathClassifier = createPathClassifier(repoPath);
|
|
49
54
|
const pathClassifications = new Map();
|
|
50
55
|
const classifyPath = (file) => {
|
|
@@ -73,68 +78,78 @@ export async function runInternalAudit(repoPath, { graph, advisoryStorePath, ski
|
|
|
73
78
|
const unusedExports = computeUnusedExports(graph, sources, { dynamicTargets, entrySet: entries });
|
|
74
79
|
const reachable = computeReachability(graph, entries);
|
|
75
80
|
const configTexts = collectConfigTexts(repoPath);
|
|
81
|
+
const npmDependencyImports = externalImports.filter((entry) => entry.ecosystem
|
|
82
|
+
? entry.ecosystem === "npm"
|
|
83
|
+
: !/\.(?:java|go|py)$/i.test(entry.file || ""));
|
|
76
84
|
const dep = computeScopedDepFindings({
|
|
77
|
-
externalImports
|
|
78
|
-
|
|
85
|
+
externalImports: npmDependencyImports,
|
|
86
|
+
packageScopes,
|
|
87
|
+
workspacePkgNames: workspacePkgNames(repoPath, pkg),
|
|
88
|
+
configTexts,
|
|
89
|
+
sourceTexts: sources,
|
|
90
|
+
nonRuntimeRoots,
|
|
91
|
+
sourceFiles: [...sources.keys()],
|
|
79
92
|
});
|
|
80
|
-
// non-npm ecosystems: Go (go.mod) + Python (requirements/pyproject/Pipfile) — same findings shape
|
|
81
93
|
const goModText = readRepoText(boundary, "go.mod");
|
|
82
94
|
const goDep = computeGoDepFindings({ externalImports, goMod: goModText != null ? parseGoMod(goModText) : null, nonRuntimeRoots });
|
|
83
|
-
const asList = (
|
|
84
|
-
const pyRules = rules.python || {};
|
|
85
|
-
const depRules = rules.dependencies || {};
|
|
95
|
+
const asList = (value) => Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
96
|
+
const pyRules = rules.python || {}, depRules = rules.dependencies || {};
|
|
86
97
|
const managedPython = [...new Set([
|
|
87
|
-
...asList(rules.managedPythonDependencies), ...asList(pyRules.managed), ...asList(pyRules.managedDependencies),
|
|
88
|
-
...asList(depRules.managedPython),
|
|
98
|
+
...asList(rules.managedPythonDependencies), ...asList(pyRules.managed), ...asList(pyRules.managedDependencies), ...asList(depRules.managedPython),
|
|
89
99
|
])];
|
|
90
100
|
const ignoredPython = [...new Set([
|
|
91
|
-
...asList(rules.ignorePythonDependencies), ...asList(pyRules.ignore), ...asList(pyRules.ignoreDependencies),
|
|
92
|
-
...asList(depRules.ignorePython),
|
|
101
|
+
...asList(rules.ignorePythonDependencies), ...asList(pyRules.ignore), ...asList(pyRules.ignoreDependencies), ...asList(depRules.ignorePython),
|
|
93
102
|
])];
|
|
103
|
+
const pyManifest = collectPyManifest(repoPath);
|
|
94
104
|
const pyDep = computePyDepFindings({
|
|
95
|
-
externalImports,
|
|
96
|
-
|
|
105
|
+
externalImports,
|
|
106
|
+
pyManifest,
|
|
107
|
+
configTexts,
|
|
108
|
+
managedDependencies: managedPython,
|
|
109
|
+
ignoredDependencies: ignoredPython,
|
|
110
|
+
nonRuntimeRoots,
|
|
97
111
|
});
|
|
112
|
+
const jvmDependencies = collectJvmDependencyEvidence(repoPath, { files: repoFiles });
|
|
98
113
|
|
|
99
|
-
|
|
100
|
-
// (the depcruise-config analogue); no bundled default rules — cycles+orphans are always on.
|
|
101
|
-
const externalImportFiles = new Set(externalImports.filter((e) => e.pkg && !e.builtin).map((e) => e.file));
|
|
114
|
+
const externalImportFiles = new Set(externalImports.filter((entry) => entry.pkg && !entry.builtin).map((entry) => entry.file));
|
|
102
115
|
const structure = computeStructureFindings(graph, { rules, entrySet: entries, externalImportFiles });
|
|
116
|
+
const correctness = analyzeSourceCorrectness(sources, { isNonProductPath });
|
|
117
|
+
const findings = [...dep.findings, ...goDep.findings, ...pyDep.findings, ...correctness.findings];
|
|
118
|
+
const deadFileSet = new Set(dead.deadFiles.map((finding) => finding.file));
|
|
119
|
+
for (const finding of structure.findings) {
|
|
120
|
+
if (!(finding.rule === "orphan-file" && deadFileSet.has(finding.file))) findings.push(finding);
|
|
121
|
+
}
|
|
103
122
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (const f of structure.findings) if (!(f.rule === "orphan-file" && deadFileSet.has(f.file))) findings.push(f);
|
|
108
|
-
const actionableDeadFiles = dead.deadFiles.filter((f) => !isNonProductPath(f.file));
|
|
109
|
-
for (const f of actionableDeadFiles) {
|
|
110
|
-
if (entries.has(f.file) || dynamicTargets.has(f.file) || TEST_FILE_RE.test(f.file)) continue;
|
|
123
|
+
const actionableDeadFiles = dead.deadFiles.filter((finding) => !isNonProductPath(finding.file));
|
|
124
|
+
for (const finding of actionableDeadFiles) {
|
|
125
|
+
if (entries.has(finding.file) || dynamicTargets.has(finding.file) || TEST_FILE_RE.test(finding.file)) continue;
|
|
111
126
|
findings.push(makeFinding({
|
|
112
127
|
category: "unused",
|
|
113
128
|
rule: "unused-file",
|
|
114
129
|
severity: "low",
|
|
115
|
-
confidence: reachable.has(
|
|
116
|
-
title: `Unused file: ${
|
|
117
|
-
detail: `${
|
|
118
|
-
file:
|
|
119
|
-
graphNodeId:
|
|
130
|
+
confidence: reachable.has(finding.file) ? "medium" : "high",
|
|
131
|
+
title: `Unused file: ${finding.file}`,
|
|
132
|
+
detail: `${finding.reason}${reachable.has(finding.file) ? "" : "; also unreachable from every entry point"}. Dynamic loading and framework conventions can't be fully ruled out — review before deleting.`,
|
|
133
|
+
file: finding.file,
|
|
134
|
+
graphNodeId: finding.file,
|
|
120
135
|
source: "internal",
|
|
121
136
|
fixHint: "review, then delete the file",
|
|
122
137
|
}));
|
|
123
138
|
}
|
|
124
139
|
let unusedExportCount = 0;
|
|
125
|
-
for (const
|
|
126
|
-
if (
|
|
127
|
-
if (/(^|\/)[^/]*\.config\.[a-z0-9]+$|(^|\/)\.[^/]+rc(\.[a-z]+)?$/i.test(
|
|
140
|
+
for (const symbol of unusedExports) {
|
|
141
|
+
if (symbol.test || isNonProductPath(symbol.file)) continue;
|
|
142
|
+
if (/(^|\/)[^/]*\.config\.[a-z0-9]+$|(^|\/)\.[^/]+rc(\.[a-z]+)?$/i.test(symbol.file)) continue;
|
|
128
143
|
findings.push(makeFinding({
|
|
129
144
|
category: "unused",
|
|
130
145
|
rule: "unused-export",
|
|
131
146
|
severity: "info",
|
|
132
147
|
confidence: "medium",
|
|
133
|
-
title: `Unused export: ${
|
|
134
|
-
detail: `${
|
|
135
|
-
file:
|
|
136
|
-
symbol:
|
|
137
|
-
graphNodeId:
|
|
148
|
+
title: `Unused export: ${symbol.label.replace(/\(\)$/, "")} — ${symbol.file}`,
|
|
149
|
+
detail: `${symbol.reason}. Either remove the export keyword (if used only internally) or delete the symbol.`,
|
|
150
|
+
file: symbol.file,
|
|
151
|
+
symbol: symbol.label,
|
|
152
|
+
graphNodeId: symbol.id,
|
|
138
153
|
source: "internal",
|
|
139
154
|
}));
|
|
140
155
|
unusedExportCount++;
|
|
@@ -155,120 +170,34 @@ export async function runInternalAudit(repoPath, { graph, advisoryStorePath, ski
|
|
|
155
170
|
}));
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
inst = collectInstalled(repoPath);
|
|
169
|
-
installedCount = inst.installed.length;
|
|
170
|
-
const store = advisoryStorePath ? loadStore(advisoryStorePath) : loadStore();
|
|
171
|
-
// Only a per-repo stamp proves that this repository's installed versions were queried. A legacy
|
|
172
|
-
// global fetched_at may belong to another repo and must never certify this one as clean.
|
|
173
|
-
const repoStamp = store.meta?.repos?.[repoPath] || null;
|
|
174
|
-
advisoryDbDate = typeof repoStamp === "string" ? repoStamp : repoStamp?.fetched_at || null;
|
|
175
|
-
if (advisoryDbDate) {
|
|
176
|
-
let status = typeof repoStamp === "object" && ["OK", "PARTIAL", "ERROR"].includes(repoStamp.status) ? repoStamp.status : "PARTIAL";
|
|
177
|
-
const fingerprintMatches = typeof repoStamp === "object" && repoStamp.query_fingerprint === advisoryQueryFingerprint(inst.installed);
|
|
178
|
-
if (!fingerprintMatches && status === "OK") status = "PARTIAL";
|
|
179
|
-
const coverage = typeof repoStamp === "object" && Number.isFinite(repoStamp.queried)
|
|
180
|
-
? ` (${repoStamp.queried_ok ?? repoStamp.queried}/${repoStamp.queried} package versions queried successfully)`
|
|
181
|
-
: "";
|
|
182
|
-
const drift = fingerprintMatches ? "" : " Dependency versions changed, or this is a legacy stamp without a package fingerprint; enable the osv profile (or advisories capability) and call refresh_advisories for complete coverage.";
|
|
183
|
-
checks.osv = {
|
|
184
|
-
status,
|
|
185
|
-
detail: `${status === "PARTIAL" ? "Partially matched" : "Matched"} installed packages against the cached OSV snapshot from ${advisoryDbDate}${coverage}.${drift}`,
|
|
186
|
-
checkedAt: advisoryDbDate,
|
|
187
|
-
};
|
|
188
|
-
for (const h of matchAdvisories(inst.installed, (eco, name) => queryStore(store, eco, name))) {
|
|
189
|
-
const mal = h.adv.kind === "malicious";
|
|
190
|
-
const reachability = packageReachability(externalImports, h.pkg.name, { isNonProductPath });
|
|
191
|
-
const observedInProduct = reachability.state === "DIRECT_RUNTIME_IMPORT";
|
|
192
|
-
const reachabilityDetail = observedInProduct
|
|
193
|
-
? ` Graph reachability: directly imported by product code in ${reachability.directRuntimeImports} callsite(s) across ${reachability.files.length} file(s).`
|
|
194
|
-
: ` Graph reachability: ${reachability.state}; ${reachability.note}`;
|
|
195
|
-
findings.push(makeFinding({
|
|
196
|
-
category: mal ? "malware" : "vulnerability",
|
|
197
|
-
rule: mal ? "malicious-package" : "known-vuln",
|
|
198
|
-
severity: mal || observedInProduct ? (mal ? "critical" : h.adv.severity) : LOWER_SEVERITY[h.adv.severity] || h.adv.severity,
|
|
199
|
-
confidence: mal || observedInProduct ? h.confidence : "low",
|
|
200
|
-
title: `${mal ? "Known-malicious package" : `Known vulnerability (${h.adv.id})`}: ${h.pkg.name}@${h.pkg.version}`,
|
|
201
|
-
detail: `${h.adv.summary || h.adv.id}${h.adv.fixedIn.length ? ` Fixed in: ${h.adv.fixedIn.join(", ")}.` : mal ? " Remove this package immediately and rotate any secrets it could reach." : ""} (matched by ${h.matchedBy}${h.adv.aliases.length ? `; aliases ${h.adv.aliases.join(", ")}` : ""}).${reachabilityDetail}`,
|
|
202
|
-
package: h.pkg.name,
|
|
203
|
-
version: h.pkg.version,
|
|
204
|
-
reachability,
|
|
205
|
-
evidence: [
|
|
206
|
-
{ file: h.adv.url, line: 0, snippet: `installed via ${h.pkg.source}${h.pkg.dev ? " (dev)" : ""}` },
|
|
207
|
-
...reachability.evidence.slice(0, 5).map((item) => ({file: item.file, line: item.line, snippet: `${item.typeOnly ? "type-only " : ""}${item.kind}`})),
|
|
208
|
-
],
|
|
209
|
-
source: "osv",
|
|
210
|
-
fixHint: mal ? `npm uninstall ${h.pkg.name} + audit what it touched` : h.adv.fixedIn.length ? `upgrade ${h.pkg.name} to ${h.adv.fixedIn[h.adv.fixedIn.length - 1]}+` : "no fixed version published — consider replacing the package",
|
|
211
|
-
}));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// direct-dependency typosquat (dev-chosen names, small set → low FP): surface quietly even alone.
|
|
215
|
-
const directDependencyNames = new Set(packageScopes.flatMap((s) => Object.keys({ ...(s.pkg?.dependencies || {}), ...(s.pkg?.devDependencies || {}) })));
|
|
216
|
-
for (const name of directDependencyNames) {
|
|
217
|
-
const sq = classifyTyposquat(name);
|
|
218
|
-
if (!sq) continue;
|
|
219
|
-
findings.push(makeFinding({
|
|
220
|
-
category: "malware",
|
|
221
|
-
rule: "typosquat",
|
|
222
|
-
severity: "medium",
|
|
223
|
-
confidence: "low",
|
|
224
|
-
title: `Possible typosquat: ${name} (looks like "${sq.nearest}")`,
|
|
225
|
-
detail: `Direct dependency "${name}" is edit-distance ${sq.distance} from the popular package "${sq.nearest}". Confirm you meant "${name}" and not "${sq.nearest}" — name-confusion is a common supply-chain lure.`,
|
|
226
|
-
package: name,
|
|
227
|
-
source: "internal",
|
|
228
|
-
fixHint: `verify "${name}" is the intended package (not a typo of "${sq.nearest}")`,
|
|
229
|
-
}));
|
|
230
|
-
}
|
|
231
|
-
for (const d of inst.drift.slice(0, 20)) {
|
|
232
|
-
findings.push(makeFinding({
|
|
233
|
-
category: "malware",
|
|
234
|
-
rule: "lockfile-drift",
|
|
235
|
-
severity: "low",
|
|
236
|
-
confidence: "medium",
|
|
237
|
-
title: `Lockfile drift: ${d.name} (locked ${d.locked}, installed ${d.installed})`,
|
|
238
|
-
detail: "The version on disk differs from the lockfile — a stale install, a manual edit, or (worst case) tampering. Reinstall from the lockfile to realign.",
|
|
239
|
-
package: d.name,
|
|
240
|
-
version: d.installed,
|
|
241
|
-
source: "internal",
|
|
242
|
-
fixHint: "npm ci (clean install from the lockfile)",
|
|
243
|
-
}));
|
|
244
|
-
}
|
|
245
|
-
} catch (error) {
|
|
246
|
-
checks.osv = { status: "ERROR", detail: `Offline advisory matching failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ---- malware heuristics: install-script beacons / miners / exfil / obfuscation across installed libs.
|
|
250
|
-
// Local + offline (ripgrep or a bounded Node fallback). Scans node_modules, Python venvs, Go vendor/cache.
|
|
251
|
-
let malwareScan = null;
|
|
252
|
-
if (!skipMalwareScan) {
|
|
253
|
-
try {
|
|
254
|
-
const importedPkgs = new Set(externalImports.filter((e) => e.pkg && !e.builtin).map((e) => e.pkg));
|
|
255
|
-
const scan = await scanMalware(repoPath, { installed: inst.installed, importedPkgs, malwareExclusions, rgPath });
|
|
256
|
-
findings.push(...scan.findings);
|
|
257
|
-
malwareScan = { scanMode: scan.scanMode, packagesScanned: scan.packagesScanned, findings: scan.findings.length, excludedSignals: scan.excludedSignals || 0 };
|
|
258
|
-
checks.malware = { status: "OK", detail: `Scanned ${scan.packagesScanned} installed package(s) using ${scan.scanMode}.` };
|
|
259
|
-
} catch (error) {
|
|
260
|
-
checks.malware = { status: "ERROR", detail: `Installed-package malware scan failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
173
|
+
const supplyChain = await runSupplyChainChecks(repoPath, {
|
|
174
|
+
externalImports,
|
|
175
|
+
packageScopes,
|
|
176
|
+
isNonProductPath,
|
|
177
|
+
advisoryStorePath,
|
|
178
|
+
skipMalwareScan,
|
|
179
|
+
malwareExclusions,
|
|
180
|
+
rgPath,
|
|
181
|
+
});
|
|
182
|
+
findings.push(...supplyChain.findings);
|
|
264
183
|
const sorted = sortFindings(findings);
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
184
|
+
const dependencyHealth = buildDependencyHealth({
|
|
185
|
+
repoPath,
|
|
186
|
+
graph,
|
|
187
|
+
repoFiles,
|
|
188
|
+
pyManifest,
|
|
189
|
+
dep,
|
|
190
|
+
goDep,
|
|
191
|
+
pyDep,
|
|
192
|
+
jvmDependencies,
|
|
193
|
+
externalImports,
|
|
194
|
+
findings: sorted,
|
|
195
|
+
packageScopes,
|
|
196
|
+
sourceFiles: [...sources.keys()],
|
|
197
|
+
correctnessCoverage: correctness.coverage,
|
|
198
|
+
checks: supplyChain.checks,
|
|
199
|
+
});
|
|
200
|
+
|
|
272
201
|
return {
|
|
273
202
|
ok: true,
|
|
274
203
|
engine: "internal",
|
|
@@ -278,14 +207,14 @@ export async function runInternalAudit(repoPath, { graph, advisoryStorePath, ski
|
|
|
278
207
|
scanned: {
|
|
279
208
|
files: dead.stats.files,
|
|
280
209
|
symbols: dead.stats.symbols,
|
|
281
|
-
manifestDeps:
|
|
210
|
+
manifestDeps: dependencyHealth.manifestDeps,
|
|
282
211
|
externalImports: externalImports.length,
|
|
283
212
|
nodeModulesPresent: boundary.resolve("node_modules").ok,
|
|
284
|
-
installedPackages: installedCount,
|
|
285
|
-
advisoryDbDate,
|
|
286
|
-
advisoryStatus: checks.osv.status,
|
|
287
|
-
malwareScanMode: malwareScan?.scanMode || "skipped",
|
|
288
|
-
malwareStatus: checks.malware.status,
|
|
213
|
+
installedPackages: supplyChain.installedCount,
|
|
214
|
+
advisoryDbDate: supplyChain.advisoryDbDate,
|
|
215
|
+
advisoryStatus: supplyChain.checks.osv.status,
|
|
216
|
+
malwareScanMode: supplyChain.malwareScan?.scanMode || "skipped",
|
|
217
|
+
malwareStatus: supplyChain.checks.malware.status,
|
|
289
218
|
packageScopes: packageScopes.length,
|
|
290
219
|
managedPythonDependencies: managedPython.length,
|
|
291
220
|
nonRuntimeRoots,
|
|
@@ -298,24 +227,7 @@ export async function runInternalAudit(repoPath, { graph, advisoryStorePath, ski
|
|
|
298
227
|
},
|
|
299
228
|
summary: summarizeFindings(sorted),
|
|
300
229
|
findings: sorted,
|
|
301
|
-
dependencyReport:
|
|
302
|
-
status: dependencyStatus,
|
|
303
|
-
evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
|
|
304
|
-
perFindingVerification: true,
|
|
305
|
-
verificationCoverage: { npm: "COMPLETE_FOR_GRAPH_SCOPE", go: "SUMMARY_ONLY", python: "SUMMARY_ONLY" },
|
|
306
|
-
declared: dep.declared.size + goDep.declared.size + pyDep.declared.size,
|
|
307
|
-
importedPackages: importedPackages.size,
|
|
308
|
-
importRecords: externalImports.length,
|
|
309
|
-
unused: dependencyFindings.filter((finding) => finding.rule === "unused-dep").length,
|
|
310
|
-
missing: dependencyFindings.filter((finding) => finding.rule === "missing-dep").length,
|
|
311
|
-
duplicateDeclarations: dependencyFindings.filter((finding) => finding.rule === "duplicate-dep").length,
|
|
312
|
-
unusedRequiringReview: dependencyFindings.filter((finding) => finding.rule === "unused-dep" && finding.verification?.decision === "REVIEW_REQUIRED").length,
|
|
313
|
-
missingWithSourceEvidence: dependencyFindings.filter((finding) => finding.rule === "missing-dep" && finding.verification?.indexedSourceImports?.status === "FOUND").length,
|
|
314
|
-
packageScopes: packageScopes.length,
|
|
315
|
-
reason: dependencyStatus === "COMPLETE"
|
|
316
|
-
? "All discovered dependency manifests were compared with the complete indexed import set; every npm unused/missing/duplicate finding carries manifest and source/config verification state."
|
|
317
|
-
: "The dependency result is scoped to a partial graph and is not a repository-wide clean bill.",
|
|
318
|
-
},
|
|
230
|
+
dependencyReport: dependencyHealth.dependencyReport,
|
|
319
231
|
deadReport: {
|
|
320
232
|
deadSymbols: dead.deadSymbols.filter((symbol) => !isNonProductPath(symbol.file)).length,
|
|
321
233
|
deadFiles: actionableDeadFiles.length,
|
|
@@ -328,7 +240,9 @@ export async function runInternalAudit(repoPath, { graph, advisoryStorePath, ski
|
|
|
328
240
|
truncated: conventionEvidence.length > 100,
|
|
329
241
|
},
|
|
330
242
|
structureReport: structure.stats,
|
|
331
|
-
|
|
332
|
-
|
|
243
|
+
sourceCorrectnessReport: correctness.coverage,
|
|
244
|
+
healthCapabilities: dependencyHealth.healthCapabilities,
|
|
245
|
+
checks: supplyChain.checks,
|
|
246
|
+
malwareScan: supplyChain.malwareScan,
|
|
333
247
|
};
|
|
334
248
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Maven/Gradle manifest presence and bounded declaration counts. We intentionally do not map Java
|
|
2
|
+
// imports to artifacts: package-to-artifact resolution needs a real build model and claiming 0/0 from
|
|
3
|
+
// source regexes would be worse than an explicit NOT_SUPPORTED/PARTIAL state.
|
|
4
|
+
import { createRepoBoundary } from "../repo-path.js";
|
|
5
|
+
import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
|
|
6
|
+
|
|
7
|
+
function mavenDeclarations(text) {
|
|
8
|
+
const source = String(text || "")
|
|
9
|
+
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
10
|
+
.replace(/<dependencyManagement\b[\s\S]*?<\/dependencyManagement>/gi, " ");
|
|
11
|
+
const identities = [];
|
|
12
|
+
const re = /<dependency\b[^>]*>([\s\S]*?)<\/dependency>/gi;
|
|
13
|
+
let match;
|
|
14
|
+
while ((match = re.exec(source))) {
|
|
15
|
+
const group = /<groupId>\s*([^<]+?)\s*<\/groupId>/i.exec(match[1])?.[1]?.trim() || "";
|
|
16
|
+
const artifact = /<artifactId>\s*([^<]+?)\s*<\/artifactId>/i.exec(match[1])?.[1]?.trim() || "";
|
|
17
|
+
if (artifact) identities.push(group ? `${group}:${artifact}` : artifact);
|
|
18
|
+
}
|
|
19
|
+
return identities;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function gradleDeclarations(text) {
|
|
23
|
+
const source = String(text || "")
|
|
24
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
25
|
+
.replace(/(^|\s)\/\/.*$/gm, "$1");
|
|
26
|
+
const declarations = [];
|
|
27
|
+
const configurations = "api|implementation|compileOnly|runtimeOnly|annotationProcessor|kapt|testImplementation|testCompileOnly|testRuntimeOnly|androidTestImplementation";
|
|
28
|
+
const line = new RegExp(`^\\s*(?:${configurations})\\s*(?:\\(\\s*)?([^\\r\\n]+)`, "gmi");
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = line.exec(source))) {
|
|
31
|
+
const expression = match[1].trim().replace(/[),;]+\s*$/, "");
|
|
32
|
+
const coordinate = /["']([^"']+:[^"']+)["']/.exec(expression)?.[1] || "";
|
|
33
|
+
const catalog = /\blibs(?:\.[A-Za-z_]\w*)+/.exec(expression)?.[0] || "";
|
|
34
|
+
declarations.push(coordinate || catalog || "unresolved-declaration");
|
|
35
|
+
}
|
|
36
|
+
return declarations;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function collectJvmDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot) } = {}) {
|
|
40
|
+
const boundary = createRepoBoundary(repoRoot);
|
|
41
|
+
const mavenFiles = files.filter((file) => /(^|\/)pom\.xml$/i.test(file));
|
|
42
|
+
const gradleFiles = files.filter((file) => /(^|\/)build\.gradle(?:\.kts)?$/i.test(file));
|
|
43
|
+
const maven = mavenFiles.flatMap((file) => mavenDeclarations(readRepoText(boundary, file)).map((identity) => ({ file, identity })));
|
|
44
|
+
const gradle = gradleFiles.flatMap((file) => gradleDeclarations(readRepoText(boundary, file)).map((identity) => ({ file, identity })));
|
|
45
|
+
return {
|
|
46
|
+
maven: {
|
|
47
|
+
present: mavenFiles.length > 0,
|
|
48
|
+
status: mavenFiles.length ? "NOT_SUPPORTED" : "NOT_PRESENT",
|
|
49
|
+
completeness: mavenFiles.length ? "PARTIAL" : "NOT_APPLICABLE",
|
|
50
|
+
manifests: mavenFiles,
|
|
51
|
+
declared: maven.length,
|
|
52
|
+
sample: maven.slice(0, 20),
|
|
53
|
+
reason: mavenFiles.length
|
|
54
|
+
? "Maven manifests and declaration counts were detected, but Java package imports were not mapped to Maven artifacts; unused/missing dependency verdicts are not supported."
|
|
55
|
+
: "No pom.xml was discovered.",
|
|
56
|
+
},
|
|
57
|
+
gradle: {
|
|
58
|
+
present: gradleFiles.length > 0,
|
|
59
|
+
status: gradleFiles.length ? "NOT_SUPPORTED" : "NOT_PRESENT",
|
|
60
|
+
completeness: gradleFiles.length ? "PARTIAL" : "NOT_APPLICABLE",
|
|
61
|
+
manifests: gradleFiles,
|
|
62
|
+
declared: gradle.length,
|
|
63
|
+
sample: gradle.slice(0, 20),
|
|
64
|
+
reason: gradleFiles.length
|
|
65
|
+
? "Gradle manifests and bounded dependency declarations were detected, but version catalogs/build logic and Java package-to-artifact mapping were not resolved; unused/missing dependency verdicts are not supported."
|
|
66
|
+
: "No build.gradle/build.gradle.kts was discovered.",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {makeFinding} from '../findings.js'
|
|
2
|
+
|
|
3
|
+
const RETRY_SIGNAL = /\b(?:retry|retries|retrying|reconnect|backoff|attempts?)\b/i
|
|
4
|
+
const WAIT_SIGNAL = /\b(?:sleep|delay)\s*\(/i
|
|
5
|
+
const FAILURE_SIGNAL = /\b(?:catch|isError|error|exception|failed?|healthCheck)\b/i
|
|
6
|
+
const TERMINATION_POLICY = /\b(?:max(?:imum)?(?:Retries|Attempts)|retryLimit|attemptLimit|deadline|timeout|isInterrupted)\b|\bcontext\s*\.\s*Done\b|\bsignal\s*\.\s*aborted\b|\battempts?\s*(?:>=|>)\s*\w+|\bbreak\s*;/i
|
|
7
|
+
|
|
8
|
+
const lineAt = (text, index) => {
|
|
9
|
+
let line = 1
|
|
10
|
+
for (let i = 0; i < index && i < text.length; i++) if (text[i] === '\n') line++
|
|
11
|
+
return line
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const lineText = (text, index) => {
|
|
15
|
+
const start = text.lastIndexOf('\n', Math.max(0, index - 1)) + 1
|
|
16
|
+
const end = text.indexOf('\n', index)
|
|
17
|
+
return text.slice(start, end < 0 ? text.length : end).trim().slice(0, 300)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function balancedEnd(text, openAt) {
|
|
21
|
+
if (text[openAt] !== '{') return -1
|
|
22
|
+
let depth = 0
|
|
23
|
+
for (let i = openAt; i < text.length; i++) {
|
|
24
|
+
if (text[i] === '{') depth++
|
|
25
|
+
else if (text[i] === '}' && --depth === 0) return i + 1
|
|
26
|
+
}
|
|
27
|
+
return -1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function retryFinding(text, file, index) {
|
|
31
|
+
return makeFinding({
|
|
32
|
+
category: 'structure',
|
|
33
|
+
rule: 'unbounded-retry-loop',
|
|
34
|
+
severity: 'low',
|
|
35
|
+
confidence: 'medium',
|
|
36
|
+
title: 'Retry/poll loop has no visible attempt, deadline, or cancellation bound',
|
|
37
|
+
detail: 'A repeating loop contains retry/failure plus wait behavior but no local maximum-attempt, deadline, cancellation, interrupt, or explicit break policy was recognized. This is a review signal, not proof of an infinite loop; success or a called helper may terminate it.',
|
|
38
|
+
file,
|
|
39
|
+
line: lineAt(text, index),
|
|
40
|
+
evidence: [{file, line: lineAt(text, index), snippet: lineText(text, index)}],
|
|
41
|
+
fixHint: 'confirm and document the retry termination/cancellation policy',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function retryBehavior(source) {
|
|
46
|
+
return RETRY_SIGNAL.test(source) || (WAIT_SIGNAL.test(source) && FAILURE_SIGNAL.test(source))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bracedRetryFindings(text, masked, file) {
|
|
50
|
+
const findings = []
|
|
51
|
+
const re = /\bwhile\s*\(([^)]*)\)\s*\{|\bfor\s*\(\s*;\s*;\s*\)\s*\{|\bfor\s*\{|\bdo\s*\{/g
|
|
52
|
+
let match
|
|
53
|
+
while ((match = re.exec(masked))) {
|
|
54
|
+
const open = re.lastIndex - 1
|
|
55
|
+
const end = balancedEnd(masked, open)
|
|
56
|
+
if (end < 0) continue
|
|
57
|
+
const body = masked.slice(open + 1, end - 1)
|
|
58
|
+
const context = masked.slice(Math.max(0, match.index - 250), match.index)
|
|
59
|
+
let condition = match[1] || '', tailLength = 0
|
|
60
|
+
if (/^do\b/.test(match[0])) {
|
|
61
|
+
const tail = /^\s*while\s*\(([^)]*)\)\s*;/.exec(masked.slice(end))
|
|
62
|
+
condition = tail?.[1] || ''
|
|
63
|
+
tailLength = tail?.[0]?.length || 0
|
|
64
|
+
}
|
|
65
|
+
const evidence = `${context}\n${condition}\n${body}`
|
|
66
|
+
if (retryBehavior(evidence) && !TERMINATION_POLICY.test(`${condition}\n${body}`)) {
|
|
67
|
+
findings.push(retryFinding(text, file, match.index))
|
|
68
|
+
}
|
|
69
|
+
re.lastIndex = end + tailLength
|
|
70
|
+
}
|
|
71
|
+
return findings
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pythonRetryFindings(text, masked, file) {
|
|
75
|
+
const findings = [], lines = masked.split(/\r?\n/)
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const match = /^(\s*)while\s+True\s*:/.exec(lines[i])
|
|
78
|
+
if (!match) continue
|
|
79
|
+
const indent = match[1].length
|
|
80
|
+
let end = i + 1
|
|
81
|
+
while (end < lines.length && (!lines[end].trim() || /^\s*/.exec(lines[end])[0].length > indent)) end++
|
|
82
|
+
const body = lines.slice(i + 1, end).join('\n')
|
|
83
|
+
const context = lines.slice(Math.max(0, i - 8), i).join('\n')
|
|
84
|
+
if (retryBehavior(`${context}\n${body}`) && !TERMINATION_POLICY.test(body)) {
|
|
85
|
+
findings.push(retryFinding(text, file, text.split(/\r?\n/).slice(0, i).join('\n').length + (i ? 1 : 0)))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return findings
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function retryFindings(text, masked, file, {python = false} = {}) {
|
|
92
|
+
return python ? pythonRetryFindings(text, masked, file) : bracedRetryFindings(text, masked, file)
|
|
93
|
+
}
|