weavatrix 0.3.13 → 1.0.0
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/LICENSE +1 -0
- package/README.md +231 -199
- package/bin/native/darwin-arm64/weavatrix +0 -0
- package/bin/native/darwin-x64/weavatrix +0 -0
- package/bin/native/linux-arm64/weavatrix +0 -0
- package/bin/native/linux-x64/weavatrix +0 -0
- package/bin/native/win32-arm64/weavatrix.exe +0 -0
- package/bin/native/win32-x64/weavatrix.exe +0 -0
- package/bin/resolve-binary.mjs +73 -0
- package/bin/run-native.mjs +26 -0
- package/bin/weavatrix-mcp.mjs +7 -6
- package/bin/weavatrix.mjs +5 -0
- package/package.json +8 -65
- package/skill/SKILL.md +62 -374
- package/skill/agents/openai.yaml +4 -0
- package/skill/references/tool-routing.md +45 -0
- package/SECURITY.md +0 -101
- package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
- package/docs/agent-task-benchmark.md +0 -58
- package/docs/releases/v0.2.19.md +0 -33
- package/docs/releases/v0.3.0.md +0 -43
- package/docs/releases/v0.3.1.md +0 -30
- package/docs/releases/v0.3.10.md +0 -76
- package/docs/releases/v0.3.11.md +0 -55
- package/docs/releases/v0.3.12.md +0 -44
- package/docs/releases/v0.3.13.md +0 -34
- package/docs/releases/v0.3.2.md +0 -15
- package/docs/releases/v0.3.3.md +0 -25
- package/docs/releases/v0.3.4.md +0 -40
- package/docs/releases/v0.3.5.md +0 -50
- package/docs/releases/v0.3.6.md +0 -44
- package/docs/releases/v0.3.7.md +0 -37
- package/docs/releases/v0.3.8.md +0 -21
- package/docs/releases/v0.3.9.md +0 -36
- package/scripts/run-agent-task-benchmark.mjs +0 -118
- package/src/analysis/allowed-test-runner.js +0 -70
- package/src/analysis/architecture/contract-graph.js +0 -119
- package/src/analysis/architecture/contract-schema.js +0 -110
- package/src/analysis/architecture/contract-storage.js +0 -35
- package/src/analysis/architecture/contract-verification.js +0 -168
- package/src/analysis/architecture-contract.js +0 -16
- package/src/analysis/audit-debt.js +0 -94
- package/src/analysis/audit-extensions.js +0 -60
- package/src/analysis/cargo-dependency-evidence.js +0 -108
- package/src/analysis/cargo-manifests.js +0 -81
- package/src/analysis/change-classification/diff-parser.js +0 -103
- package/src/analysis/change-classification/options.js +0 -40
- package/src/analysis/change-classification/symbol-classifier.js +0 -177
- package/src/analysis/change-classification.js +0 -188
- package/src/analysis/coverage-reports.js +0 -235
- package/src/analysis/cycle-route.js +0 -14
- package/src/analysis/data-flow-evidence.js +0 -114
- package/src/analysis/dead-check.js +0 -300
- package/src/analysis/dead-code-review/candidates.js +0 -194
- package/src/analysis/dead-code-review/policy.js +0 -23
- package/src/analysis/dead-code-review.js +0 -119
- package/src/analysis/dep-check-ecosystems.js +0 -205
- package/src/analysis/dep-check.js +0 -290
- package/src/analysis/dep-rules.js +0 -10
- package/src/analysis/dependency/conventions.js +0 -16
- package/src/analysis/dependency/scoped-dependencies.js +0 -51
- package/src/analysis/dependency/source-references.js +0 -21
- package/src/analysis/duplicate-groups.js +0 -104
- package/src/analysis/duplicates-worker.js +0 -11
- package/src/analysis/duplicates.compute.js +0 -269
- package/src/analysis/duplicates.js +0 -15
- package/src/analysis/duplicates.run.js +0 -52
- package/src/analysis/duplicates.tokenize.js +0 -199
- package/src/analysis/endpoints/common.js +0 -62
- package/src/analysis/endpoints/extract.js +0 -107
- package/src/analysis/endpoints/inventory.js +0 -84
- package/src/analysis/endpoints/mounts.js +0 -129
- package/src/analysis/endpoints-java.js +0 -260
- package/src/analysis/endpoints-rust.js +0 -124
- package/src/analysis/endpoints.js +0 -3
- package/src/analysis/findings.js +0 -70
- package/src/analysis/git-history/analytics.js +0 -177
- package/src/analysis/git-history/collector.js +0 -67
- package/src/analysis/git-history/options.js +0 -66
- package/src/analysis/git-history.js +0 -143
- package/src/analysis/git-ref-graph.js +0 -76
- package/src/analysis/go-dependency-evidence.js +0 -62
- package/src/analysis/graph-analysis.aggregate.js +0 -295
- package/src/analysis/graph-analysis.edges.js +0 -24
- package/src/analysis/graph-analysis.js +0 -7
- package/src/analysis/graph-analysis.refs.js +0 -154
- package/src/analysis/graph-analysis.summaries.js +0 -65
- package/src/analysis/health-capabilities.js +0 -83
- package/src/analysis/hot-path-review.js +0 -266
- package/src/analysis/http-contract-wrappers.js +0 -241
- package/src/analysis/http-contracts/analysis.js +0 -179
- package/src/analysis/http-contracts/client-call-detection.js +0 -82
- package/src/analysis/http-contracts/client-call-parser.js +0 -273
- package/src/analysis/http-contracts/graph-context.js +0 -143
- package/src/analysis/http-contracts/matching.js +0 -61
- package/src/analysis/http-contracts/shared.js +0 -85
- package/src/analysis/http-contracts.js +0 -6
- package/src/analysis/internal-audit/dependency-health.js +0 -113
- package/src/analysis/internal-audit/python-manifests.js +0 -78
- package/src/analysis/internal-audit/repo-files.js +0 -50
- package/src/analysis/internal-audit/supply-chain.js +0 -149
- package/src/analysis/internal-audit.collect.js +0 -213
- package/src/analysis/internal-audit.js +0 -9
- package/src/analysis/internal-audit.reach.js +0 -173
- package/src/analysis/internal-audit.run.js +0 -256
- package/src/analysis/java-source.js +0 -36
- package/src/analysis/jvm-artifact-index.js +0 -87
- package/src/analysis/jvm-dependency-evidence.js +0 -138
- package/src/analysis/jvm-manifests.js +0 -114
- package/src/analysis/manifests.js +0 -123
- package/src/analysis/package-reachability.js +0 -39
- package/src/analysis/source-complexity.ast.js +0 -97
- package/src/analysis/source-complexity.constants.js +0 -55
- package/src/analysis/source-complexity.js +0 -14
- package/src/analysis/source-complexity.report.js +0 -81
- package/src/analysis/source-complexity.walk.js +0 -228
- package/src/analysis/source-correctness/retry-patterns.js +0 -93
- package/src/analysis/source-correctness.js +0 -222
- package/src/analysis/static-test-reachability.js +0 -150
- package/src/analysis/structure/dependency-graph.js +0 -151
- package/src/analysis/structure/findings.js +0 -162
- package/src/analysis/task-retrieval.js +0 -107
- package/src/analysis/transport-contracts.js +0 -224
- package/src/analysis/transport-runtime-evidence.js +0 -155
- package/src/analysis-kit.mjs +0 -17
- package/src/bounds.js +0 -4
- package/src/build-graph.js +0 -209
- package/src/child-env.js +0 -7
- package/src/config.js +0 -5
- package/src/extension/local-services.mjs +0 -75
- package/src/git-exec.js +0 -67
- package/src/graph/build-worker.js +0 -49
- package/src/graph/builder/go-receiver-resolution.js +0 -112
- package/src/graph/builder/js/dynamic-import.js +0 -26
- package/src/graph/builder/js/queries.js +0 -48
- package/src/graph/builder/lang-csharp.js +0 -40
- package/src/graph/builder/lang-css.js +0 -29
- package/src/graph/builder/lang-go.js +0 -140
- package/src/graph/builder/lang-html.js +0 -29
- package/src/graph/builder/lang-java-calls.js +0 -142
- package/src/graph/builder/lang-java.js +0 -215
- package/src/graph/builder/lang-js.js +0 -291
- package/src/graph/builder/lang-python.js +0 -235
- package/src/graph/builder/lang-rust-calls.js +0 -63
- package/src/graph/builder/lang-rust.js +0 -299
- package/src/graph/builder/lang-solidity.js +0 -126
- package/src/graph/builder/lang-sql.js +0 -263
- package/src/graph/builder/pass2-resolution.js +0 -81
- package/src/graph/builder/spec-pkg.js +0 -87
- package/src/graph/community.js +0 -27
- package/src/graph/edge-provenance.js +0 -31
- package/src/graph/file-lock.js +0 -69
- package/src/graph/freshness-probe.js +0 -142
- package/src/graph/graph-filter.js +0 -63
- package/src/graph/incremental-refresh.js +0 -235
- package/src/graph/internal-builder.barrels.js +0 -235
- package/src/graph/internal-builder.build.js +0 -300
- package/src/graph/internal-builder.java.js +0 -33
- package/src/graph/internal-builder.js +0 -12
- package/src/graph/internal-builder.langs.js +0 -157
- package/src/graph/internal-builder.pass2.js +0 -163
- package/src/graph/internal-builder.resolvers.js +0 -286
- package/src/graph/layout.js +0 -51
- package/src/graph/node-id.js +0 -7
- package/src/graph/parser-artifact-boundary.js +0 -55
- package/src/graph/relations.js +0 -4
- package/src/graph/repo-registry.js +0 -124
- package/src/graph/resolvers/rust.js +0 -117
- package/src/infra/infra-items.js +0 -255
- package/src/infra/infra-registry.js +0 -184
- package/src/infra/infra.detect.js +0 -119
- package/src/infra/infra.js +0 -38
- package/src/infra/infra.match.js +0 -102
- package/src/infra/infra.scan.js +0 -93
- package/src/mcp/actions/graph-lifecycle.mjs +0 -148
- package/src/mcp/architecture-bootstrap.mjs +0 -168
- package/src/mcp/architecture-starter.mjs +0 -234
- package/src/mcp/catalog.mjs +0 -233
- package/src/mcp/company-contract-page.mjs +0 -142
- package/src/mcp/company-contract-reconcile.mjs +0 -87
- package/src/mcp/company-contract-verdict.mjs +0 -42
- package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
- package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
- package/src/mcp/evidence/package-graph-common.mjs +0 -115
- package/src/mcp/evidence/package-lock-graph.mjs +0 -92
- package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
- package/src/mcp/evidence-snapshot.common.mjs +0 -158
- package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
- package/src/mcp/evidence-snapshot.health.mjs +0 -102
- package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
- package/src/mcp/evidence-snapshot.mjs +0 -50
- package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
- package/src/mcp/evidence-snapshot.structure.mjs +0 -84
- package/src/mcp/extension-api.mjs +0 -77
- package/src/mcp/git-output.mjs +0 -7
- package/src/mcp/graph/context-core.mjs +0 -111
- package/src/mcp/graph/context-seeds.mjs +0 -207
- package/src/mcp/graph/context-state.mjs +0 -85
- package/src/mcp/graph/reverse-reach.mjs +0 -42
- package/src/mcp/graph/tools-core.mjs +0 -143
- package/src/mcp/graph/tools-query.mjs +0 -276
- package/src/mcp/graph-context.mjs +0 -5
- package/src/mcp/graph-diff.mjs +0 -218
- package/src/mcp/health/audit-format.mjs +0 -199
- package/src/mcp/health/audit.mjs +0 -174
- package/src/mcp/health/dead-code.mjs +0 -110
- package/src/mcp/health/duplicates.mjs +0 -85
- package/src/mcp/health/endpoints.mjs +0 -71
- package/src/mcp/health/structure.mjs +0 -219
- package/src/mcp/runtime-version.mjs +0 -31
- package/src/mcp/server/auto-refresh.mjs +0 -66
- package/src/mcp/server/runtime-config.mjs +0 -43
- package/src/mcp/server/shutdown.mjs +0 -40
- package/src/mcp/staleness-notice.mjs +0 -20
- package/src/mcp/sync/evidence-architecture.mjs +0 -54
- package/src/mcp/sync/evidence-common.mjs +0 -88
- package/src/mcp/sync/evidence-duplicates.mjs +0 -97
- package/src/mcp/sync/evidence-health.mjs +0 -56
- package/src/mcp/sync/evidence-packages.mjs +0 -95
- package/src/mcp/sync/payload-common.mjs +0 -97
- package/src/mcp/sync/payload-v2.mjs +0 -54
- package/src/mcp/sync/payload-v3.mjs +0 -79
- package/src/mcp/sync-evidence.mjs +0 -23
- package/src/mcp/sync-payload.mjs +0 -70
- package/src/mcp/tool-result.mjs +0 -54
- package/src/mcp/tools-actions.mjs +0 -11
- package/src/mcp/tools-architecture.mjs +0 -177
- package/src/mcp/tools-company.mjs +0 -251
- package/src/mcp/tools-context.mjs +0 -222
- package/src/mcp/tools-endpoints.mjs +0 -207
- package/src/mcp/tools-graph-hubs.mjs +0 -78
- package/src/mcp/tools-graph.mjs +0 -17
- package/src/mcp/tools-health.mjs +0 -24
- package/src/mcp/tools-history.mjs +0 -28
- package/src/mcp/tools-impact-change.mjs +0 -220
- package/src/mcp/tools-impact-precision.mjs +0 -89
- package/src/mcp/tools-impact.mjs +0 -169
- package/src/mcp/tools-source.mjs +0 -238
- package/src/mcp/tools-verified-change.mjs +0 -200
- package/src/mcp-rg.mjs +0 -50
- package/src/mcp-runtime.mjs +0 -2
- package/src/mcp-server.mjs +0 -298
- package/src/mcp-source-tools.mjs +0 -163
- package/src/path-classification.js +0 -220
- package/src/path-ignore.js +0 -69
- package/src/precision/lsp-client/constants.js +0 -12
- package/src/precision/lsp-client/environment.js +0 -20
- package/src/precision/lsp-client/errors.js +0 -15
- package/src/precision/lsp-client/lifecycle.js +0 -114
- package/src/precision/lsp-client/message-parser.js +0 -81
- package/src/precision/lsp-client/protocol.js +0 -212
- package/src/precision/lsp-client/registry.js +0 -59
- package/src/precision/lsp-client/repo-uri.js +0 -60
- package/src/precision/lsp-client/stdio-client.js +0 -140
- package/src/precision/lsp-client.js +0 -10
- package/src/precision/lsp-overlay/budget-policy.js +0 -78
- package/src/precision/lsp-overlay/build.js +0 -261
- package/src/precision/lsp-overlay/contract.js +0 -61
- package/src/precision/lsp-overlay/reference-results.js +0 -108
- package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
- package/src/precision/lsp-overlay/source-session.js +0 -142
- package/src/precision/lsp-overlay/store.js +0 -150
- package/src/precision/lsp-overlay/target-index.js +0 -152
- package/src/precision/lsp-overlay/target-query.js +0 -55
- package/src/precision/lsp-overlay.js +0 -11
- package/src/precision/symbol-query.js +0 -224
- package/src/precision/typescript-lsp-provider.js +0 -12
- package/src/precision/typescript-provider/client.js +0 -89
- package/src/precision/typescript-provider/discovery.js +0 -124
- package/src/precision/typescript-provider/isolated-runtime.js +0 -39
- package/src/precision/typescript-provider/project-host.js +0 -254
- package/src/precision/typescript-provider/project-safety.js +0 -166
- package/src/precision/typescript-provider/reference-usage.js +0 -53
- package/src/process.js +0 -95
- package/src/repo-path.js +0 -53
- package/src/scan/discover.inventory.js +0 -78
- package/src/scan/discover.js +0 -5
- package/src/scan/discover.list.js +0 -79
- package/src/scan/discover.stack.js +0 -227
- package/src/scan/search.core.js +0 -24
- package/src/scan/search.git.js +0 -102
- package/src/scan/search.js +0 -9
- package/src/scan/search.node.js +0 -83
- package/src/scan/search.preview.js +0 -49
- package/src/scan/search.rg.js +0 -145
- package/src/security/advisory-store.js +0 -172
- package/src/security/installed-jvm-rust.js +0 -46
- package/src/security/installed.js +0 -280
- package/src/security/malware-file-heuristics.js +0 -121
- package/src/security/malware-heuristics.exclusions.js +0 -134
- package/src/security/malware-heuristics.js +0 -15
- package/src/security/malware-heuristics.roots.js +0 -145
- package/src/security/malware-heuristics.scan.js +0 -89
- package/src/security/malware-heuristics.sweep.js +0 -122
- package/src/security/malware-scoring.js +0 -105
- package/src/security/match.js +0 -85
- package/src/security/registry-sig.classify.js +0 -137
- package/src/security/registry-sig.js +0 -18
- package/src/security/registry-sig.rules.js +0 -192
- package/src/security/rust-advisory-report.js +0 -60
- package/src/security/typosquat.js +0 -81
- package/src/util.js +0 -40
- package/src/version.js +0 -7
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// Content roots + path-to-package mapping for the malware scanner (split out of
|
|
2
|
-
// malware-heuristics.js): npm node_modules, Python site-packages, Go vendor/ and module cache.
|
|
3
|
-
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { delimiter, join, relative } from "node:path";
|
|
6
|
-
import { normPath } from "./malware-heuristics.exclusions.js";
|
|
7
|
-
|
|
8
|
-
const pep503 = (name) => String(name || "").toLowerCase().replace(/[-_.]+/g, "-");
|
|
9
|
-
|
|
10
|
-
// node_modules/@scope/name/... → @scope/name ; node_modules/name/... → name
|
|
11
|
-
export function pkgOfNodeModulesPath(p) {
|
|
12
|
-
const s = String(p).replace(/\\/g, "/");
|
|
13
|
-
const i = s.lastIndexOf("node_modules/");
|
|
14
|
-
if (i < 0) return "";
|
|
15
|
-
const rest = s.slice(i + "node_modules/".length).split("/");
|
|
16
|
-
return rest[0]?.startsWith("@") ? `${rest[0]}/${rest[1] || ""}` : rest[0] || "";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function existingDir(p) {
|
|
20
|
-
try { return p && existsSync(p) && statSync(p).isDirectory(); } catch { return false; }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function sitePackageRoots(repoPath) {
|
|
24
|
-
const roots = [];
|
|
25
|
-
for (const venv of [".venv", "venv", "env"]) {
|
|
26
|
-
const win = join(repoPath, venv, "Lib", "site-packages");
|
|
27
|
-
if (existingDir(win)) roots.push(win);
|
|
28
|
-
try {
|
|
29
|
-
for (const d of readdirSync(join(repoPath, venv, "lib"))) {
|
|
30
|
-
const posix = join(repoPath, venv, "lib", String(d), "site-packages");
|
|
31
|
-
if (/^python/i.test(String(d)) && existingDir(posix)) roots.push(posix);
|
|
32
|
-
}
|
|
33
|
-
} catch { /* no posix venv layout */ }
|
|
34
|
-
}
|
|
35
|
-
return [...new Set(roots.map((r) => join(r)))];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function pyTopLevelMap(sitePackagesRoot) {
|
|
39
|
-
const map = new Map();
|
|
40
|
-
let entries = [];
|
|
41
|
-
try { entries = readdirSync(sitePackagesRoot); } catch { return map; }
|
|
42
|
-
for (const e of entries) {
|
|
43
|
-
if (!/\.dist-info$/i.test(e)) continue;
|
|
44
|
-
const dist = e.replace(/-\d[\w.!+-]*\.dist-info$/i, "");
|
|
45
|
-
const canonical = pep503(dist);
|
|
46
|
-
try {
|
|
47
|
-
const top = readFileSync(join(sitePackagesRoot, e, "top_level.txt"), "utf8");
|
|
48
|
-
for (const line of top.split(/\r?\n/).map((x) => x.trim()).filter(Boolean)) map.set(line, canonical);
|
|
49
|
-
} catch { /* metadata often omits top_level.txt */ }
|
|
50
|
-
try {
|
|
51
|
-
const meta = readFileSync(join(sitePackagesRoot, e, "METADATA"), "utf8");
|
|
52
|
-
const m = meta.match(/^Name:\s*(.+)$/mi);
|
|
53
|
-
if (m) map.set(dist, pep503(m[1]));
|
|
54
|
-
} catch { /* no metadata */ }
|
|
55
|
-
}
|
|
56
|
-
return map;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function pkgOfSitePackagesPath(file, sitePackagesRoot, topMap) {
|
|
60
|
-
const rel = normPath(relative(sitePackagesRoot, file));
|
|
61
|
-
if (!rel || rel.startsWith("..")) return "";
|
|
62
|
-
const first = rel.split("/")[0] || "";
|
|
63
|
-
if (!first || first === "__pycache__") return "";
|
|
64
|
-
if (/\.dist-info$|\.egg-info$/i.test(first)) return "";
|
|
65
|
-
const top = first.replace(/\.(py|so|pyd|dll|pth)$/i, "");
|
|
66
|
-
return topMap.get(top) || pep503(top);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function vendorModuleNames(installed) {
|
|
70
|
-
return [...new Set((installed || []).filter((p) => p.ecosystem === "Go" && p.name).map((p) => String(p.name)))].sort((a, b) => b.length - a.length);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function pkgOfVendorPath(file, vendorRoot, moduleNames) {
|
|
74
|
-
const rel = normPath(relative(vendorRoot, file));
|
|
75
|
-
if (!rel || rel.startsWith("..")) return "";
|
|
76
|
-
for (const name of moduleNames) if (rel === name || rel.startsWith(`${name}/`)) return name;
|
|
77
|
-
const parts = rel.split("/");
|
|
78
|
-
if (parts[0]?.includes(".") && parts.length >= 3) return parts.slice(0, 3).join("/");
|
|
79
|
-
return parts.slice(0, 2).join("/") || parts[0] || "";
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function goCacheRoots() {
|
|
83
|
-
const roots = [];
|
|
84
|
-
if (process.env.GOMODCACHE) roots.push(...String(process.env.GOMODCACHE).split(delimiter));
|
|
85
|
-
for (const gp of String(process.env.GOPATH || join(homedir(), "go")).split(delimiter)) roots.push(join(gp, "pkg", "mod"));
|
|
86
|
-
return [...new Set(roots.filter(existingDir))];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function goModCacheEscape(modulePath) {
|
|
90
|
-
return String(modulePath || "").split("/").map((part) => part.replace(/[A-Z]/g, (c) => `!${c.toLowerCase()}`)).join("/");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function goModuleSourceRoots(installed) {
|
|
94
|
-
const roots = [];
|
|
95
|
-
const caches = goCacheRoots();
|
|
96
|
-
const goPkgs = (installed || []).filter((p) => p.ecosystem === "Go" && p.name && p.version);
|
|
97
|
-
for (const p of goPkgs) {
|
|
98
|
-
const mod = goModCacheEscape(p.name);
|
|
99
|
-
const ver = String(p.version).startsWith("v") ? String(p.version) : `v${p.version}`;
|
|
100
|
-
for (const cache of caches) {
|
|
101
|
-
const dir = join(cache, `${mod}@${ver}`);
|
|
102
|
-
if (existingDir(dir)) roots.push({ kind: "Go", root: dir, packages: 1, pathToPkg: () => p.name });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return roots;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function buildContentRoots(repoPath, installed) {
|
|
109
|
-
const roots = [];
|
|
110
|
-
const nmDir = join(repoPath, "node_modules");
|
|
111
|
-
const jsPkgDirs = existingDir(nmDir) ? listPkgDirs(nmDir) : [];
|
|
112
|
-
// Scan installed package roots, not the whole node_modules container. Package-manager caches,
|
|
113
|
-
// hosted release snapshots and other hidden metadata are not installed dependencies and may
|
|
114
|
-
// legitimately contain malware-signature fixtures (including Weavatrix's own rule source).
|
|
115
|
-
for (const item of jsPkgDirs) roots.push({ kind: "npm", root: item.dir, packages: 1, pathToPkg: pkgOfNodeModulesPath });
|
|
116
|
-
|
|
117
|
-
for (const sp of sitePackageRoots(repoPath)) {
|
|
118
|
-
const topMap = pyTopLevelMap(sp);
|
|
119
|
-
let packages = 0;
|
|
120
|
-
try { packages = readdirSync(sp).filter((e) => !e.startsWith(".") && !/\.dist-info$|\.egg-info$|__pycache__/i.test(e)).length; } catch { /* unreadable */ }
|
|
121
|
-
roots.push({ kind: "PyPI", root: sp, packages, pathToPkg: (file) => pkgOfSitePackagesPath(file, sp, topMap) });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const vendorRoot = join(repoPath, "vendor");
|
|
125
|
-
if (existingDir(vendorRoot)) {
|
|
126
|
-
const modules = vendorModuleNames(installed);
|
|
127
|
-
roots.push({ kind: "Go", root: vendorRoot, packages: modules.length || 1, pathToPkg: (file) => pkgOfVendorPath(file, vendorRoot, modules) });
|
|
128
|
-
}
|
|
129
|
-
roots.push(...goModuleSourceRoots(installed));
|
|
130
|
-
return roots;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function listPkgDirs(nmDir) {
|
|
134
|
-
const out = [];
|
|
135
|
-
let entries;
|
|
136
|
-
try { entries = readdirSync(nmDir); } catch { return out; }
|
|
137
|
-
for (const e of entries) {
|
|
138
|
-
if (e.startsWith(".")) continue;
|
|
139
|
-
if (e.startsWith("@")) {
|
|
140
|
-
let scoped; try { scoped = readdirSync(join(nmDir, e)); } catch { continue; }
|
|
141
|
-
for (const s of scoped) out.push({ pkg: `${e}/${s}`, dir: join(nmDir, e, s) });
|
|
142
|
-
} else out.push({ pkg: e, dir: join(nmDir, e) });
|
|
143
|
-
}
|
|
144
|
-
return out;
|
|
145
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// scanMalware orchestration (split out of malware-heuristics.js): phase A install scripts +
|
|
2
|
-
// lockfile origins, phase B content sweep, path-aware file heuristics, then scoring.
|
|
3
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { resolveRg } from "../scan/search.js";
|
|
6
|
-
import { classifyInstallScript, classifyResolvedUrl, isCloudSdkMetadataUse } from "./registry-sig.js";
|
|
7
|
-
import { fileHeuristicSweep } from "./malware-file-heuristics.js";
|
|
8
|
-
import { buildMalwareFindings } from "./malware-scoring.js";
|
|
9
|
-
import { parseMalwareExclusions, isManifestUrlNoise, isDocUrlNoise, isMalwareSignalExcluded, installScriptSnippet } from "./malware-heuristics.exclusions.js";
|
|
10
|
-
import { buildContentRoots, listPkgDirs } from "./malware-heuristics.roots.js";
|
|
11
|
-
import { rgSweep, nodeSweep } from "./malware-heuristics.sweep.js";
|
|
12
|
-
|
|
13
|
-
// → { findings, scanMode } ; importedPkgs = Set of packages the repo's own code imports (evidence note).
|
|
14
|
-
export async function scanMalware(repoPath, { installed = [], importedPkgs = new Set(), rgPath = "", forceNode = false, timeoutMs = 60000, malwareExclusions = {} } = {}) {
|
|
15
|
-
const nmDir = join(repoPath, "node_modules");
|
|
16
|
-
const contentRoots = buildContentRoots(repoPath, installed);
|
|
17
|
-
const byPkg = new Map(); // pkg -> [{rule signal, file, line, snippet}]
|
|
18
|
-
const exclusions = parseMalwareExclusions(malwareExclusions);
|
|
19
|
-
let excludedSignals = 0;
|
|
20
|
-
const add = (pkg, sig, file = "", line = 0, snippet = "") => {
|
|
21
|
-
if (!pkg) return;
|
|
22
|
-
const signal = { ...sig, file, line, snippet: snippet || sig.snippet || "" };
|
|
23
|
-
if (isManifestUrlNoise(file, signal) || isDocUrlNoise(file, signal) || isCloudSdkMetadataUse(pkg, signal.key) || isMalwareSignalExcluded(pkg, signal, exclusions)) {
|
|
24
|
-
excludedSignals++;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
(byPkg.get(pkg) || byPkg.set(pkg, []).get(pkg)).push(signal);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// ---- phase A: install scripts (from node_modules package.jsons) + lockfile resolved origins ----
|
|
31
|
-
const pkgDirs = existsSync(nmDir) ? listPkgDirs(nmDir) : [];
|
|
32
|
-
const lockScriptMeta = new Map(
|
|
33
|
-
(installed || [])
|
|
34
|
-
.filter((p) => p.ecosystem === "npm" && p.source === "package-lock" && p.hasInstallScript !== undefined)
|
|
35
|
-
.map((p) => [`${p.name}@${p.version}`, !!p.hasInstallScript])
|
|
36
|
-
);
|
|
37
|
-
for (const { pkg, dir } of pkgDirs) {
|
|
38
|
-
try {
|
|
39
|
-
const pj = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
40
|
-
// package-lock hasInstallScript describes install-time hooks. Development and
|
|
41
|
-
// publication hooks such as prepare do not constitute drift from that flag.
|
|
42
|
-
const scriptSnippet = installScriptSnippet(pj.scripts);
|
|
43
|
-
if (scriptSnippet && lockScriptMeta.get(`${pkg}@${pj.version || ""}`) === false) {
|
|
44
|
-
add(pkg, {
|
|
45
|
-
key: "install-script-drift",
|
|
46
|
-
severity: "high",
|
|
47
|
-
nearZeroFp: false,
|
|
48
|
-
what: "installed package has lifecycle scripts absent from package-lock metadata",
|
|
49
|
-
snippet: scriptSnippet,
|
|
50
|
-
}, join(dir, "package.json"));
|
|
51
|
-
}
|
|
52
|
-
for (const sig of classifyInstallScript(pj.scripts)) add(pkg, sig, join(dir, "package.json"));
|
|
53
|
-
} catch { /* not a package */ }
|
|
54
|
-
}
|
|
55
|
-
for (const p of installed) {
|
|
56
|
-
const sig = p.resolved ? classifyResolvedUrl(p.resolved) : null;
|
|
57
|
-
if (sig) add(p.name, sig, "package-lock.json");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---- phase B: content sweep ----
|
|
61
|
-
let scanMode = "none";
|
|
62
|
-
if (contentRoots.length) {
|
|
63
|
-
const rg = forceNode ? null : await resolveRg("auto", rgPath);
|
|
64
|
-
let swept = false;
|
|
65
|
-
if (rg) {
|
|
66
|
-
const { hits, errored, total } = await rgSweep(rg, contentRoots, timeoutMs);
|
|
67
|
-
if (errored < total) {
|
|
68
|
-
// honest label: full when every rule ran, partial when some rules failed (don't claim
|
|
69
|
-
// full coverage we didn't get); every-rule-failed falls through to the Node sweep below.
|
|
70
|
-
const kinds = [...new Set(contentRoots.map((r) => r.kind))].join("+");
|
|
71
|
-
scanMode = errored ? `ripgrep-partial (${total - errored}/${total} sweeps; ${kinds})` : `ripgrep-full (${kinds})`;
|
|
72
|
-
swept = true;
|
|
73
|
-
for (const h of hits) add(h.pkg, h.rule, h.file, h.line, h.text);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (!swept) {
|
|
77
|
-
scanMode = rg ? "node-limited (ripgrep failed)" : "node-limited";
|
|
78
|
-
for (const h of nodeSweep(nmDir, pkgDirs)) add(h.pkg, h.rule, h.file, h.line, h.text);
|
|
79
|
-
}
|
|
80
|
-
} else {
|
|
81
|
-
scanMode = "skipped";
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ---- path-aware cheap checks: things that need file names or magic bytes, not just regex text.
|
|
85
|
-
// These run regardless of rg availability and stay bounded; content regex still does the broad sweep.
|
|
86
|
-
for (const h of fileHeuristicSweep(contentRoots, pkgDirs)) add(h.pkg, h.rule, h.file, h.line, h.text);
|
|
87
|
-
|
|
88
|
-
return buildMalwareFindings({ byPkg, importedPkgs, contentRoots, scanMode, excludedSignals });
|
|
89
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// Content sweeps for the malware scanner (split out of malware-heuristics.js): a ripgrep sweep
|
|
2
|
-
// over all content roots when rg is available, else a bounded Node fallback over entry files.
|
|
3
|
-
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { runCommand } from "../process.js";
|
|
6
|
-
import { CONTENT_RULES, classifyContent } from "./registry-sig.js";
|
|
7
|
-
import { normPath } from "./malware-heuristics.exclusions.js";
|
|
8
|
-
|
|
9
|
-
const SKIP_GLOBS = [
|
|
10
|
-
"!**/*.map", "!**/*.md", "!**/*.mdx", "!**/*.markdown", "!**/*.mdown", "!**/*.mkd", "!**/*.rst", "!**/*.d.ts",
|
|
11
|
-
"!**/doc/**", "!**/docs/**", "!**/documentation/**",
|
|
12
|
-
"!**/test/**", "!**/tests/**", "!**/__tests__/**", "!**/fixtures/**", "!**/example/**", "!**/examples/**"
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
function globalRuleRegex(rule) {
|
|
16
|
-
const flags = rule.re.flags.includes("g") ? rule.re.flags : `${rule.re.flags}g`;
|
|
17
|
-
return new RegExp(rule.re.source, flags);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function matchRuleOffsets(rule, lineText, subText = "") {
|
|
21
|
-
const text = String(lineText || "");
|
|
22
|
-
if (text) {
|
|
23
|
-
const offsets = [];
|
|
24
|
-
const re = globalRuleRegex(rule);
|
|
25
|
-
let guard = 0;
|
|
26
|
-
let m;
|
|
27
|
-
while ((m = re.exec(text)) && guard++ < 24) {
|
|
28
|
-
offsets.push(m.index);
|
|
29
|
-
if (!m[0].length) re.lastIndex++;
|
|
30
|
-
}
|
|
31
|
-
if (!offsets.length && rule.key === "obfuscated-code") {
|
|
32
|
-
const at = subText ? text.indexOf(subText) : 0;
|
|
33
|
-
if (at >= 0) offsets.push(at);
|
|
34
|
-
}
|
|
35
|
-
return offsets;
|
|
36
|
-
}
|
|
37
|
-
if (subText && (rule.key === "obfuscated-code" || rule.re.test(subText))) return [0];
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function rootOfFile(file, roots) {
|
|
42
|
-
const f = normPath(file).toLowerCase();
|
|
43
|
-
return roots.find((r) => {
|
|
44
|
-
const root = normPath(r.root).toLowerCase();
|
|
45
|
-
return f === root || f.startsWith(`${root}/`);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function chunks(list, size) {
|
|
50
|
-
const out = [];
|
|
51
|
-
for (let i = 0; i < list.length; i += size) out.push(list.slice(i, i + size));
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function rgSweep(rg, contentRoots, timeoutMs) {
|
|
56
|
-
const hits = []; // {pkg, file, line, text, rule}
|
|
57
|
-
let errored = 0; // a failed/broken rule must not masquerade as "scanned, 0 hits"
|
|
58
|
-
let total = 0;
|
|
59
|
-
for (const rule of CONTENT_RULES) {
|
|
60
|
-
for (const group of chunks(contentRoots, 40)) {
|
|
61
|
-
total++;
|
|
62
|
-
const args = ["--json", "-i", "--no-ignore", "--hidden", "--follow", "--max-filesize", "2M", "--max-count", "20", ...SKIP_GLOBS.flatMap((g) => ["-g", g]), "-e", rule.pattern, "--", ...group.map((r) => r.root)];
|
|
63
|
-
let r;
|
|
64
|
-
try { r = await runCommand(rg, args, { timeoutMs }); } catch { errored++; continue; }
|
|
65
|
-
if (r.exitCode === 2 && !String(r.stdout || "").trim()) { errored++; continue; }
|
|
66
|
-
for (const line of String(r.stdout || "").split(/\r?\n/)) {
|
|
67
|
-
if (!line) continue;
|
|
68
|
-
let obj; try { obj = JSON.parse(line); } catch { continue; }
|
|
69
|
-
if (obj.type !== "match") continue;
|
|
70
|
-
const file = obj.data?.path?.text || "";
|
|
71
|
-
const root = rootOfFile(file, group);
|
|
72
|
-
const pkg = root?.pathToPkg(file) || "";
|
|
73
|
-
if (!pkg) continue;
|
|
74
|
-
const lineText = String(obj.data?.lines?.text || "");
|
|
75
|
-
const subText = String(obj.data?.submatches?.[0]?.match?.text || "");
|
|
76
|
-
for (const at of matchRuleOffsets(rule, lineText, subText)) {
|
|
77
|
-
const text = (lineText ? lineText.slice(Math.max(0, at - 160), at + 180) : subText).trim().slice(0, 340);
|
|
78
|
-
hits.push({ pkg, file, line: obj.data?.line_number || 0, text, rule });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return { hits, errored, total };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// no-rg fallback: each package's package.json main/bin entries + root JS files, bounded
|
|
87
|
-
export function nodeSweep(nmDir, pkgDirs, capFiles = 3000) {
|
|
88
|
-
const hits = [];
|
|
89
|
-
let seenFiles = 0;
|
|
90
|
-
for (const { pkg, dir } of pkgDirs) {
|
|
91
|
-
if (seenFiles >= capFiles) break;
|
|
92
|
-
let pj = null;
|
|
93
|
-
try { pj = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")); } catch { continue; }
|
|
94
|
-
const cands = new Set();
|
|
95
|
-
for (const f of [pj.main, pj.module, pj.browser]) if (typeof f === "string") cands.add(f);
|
|
96
|
-
if (pj.bin) for (const b of typeof pj.bin === "string" ? [pj.bin] : Object.values(pj.bin)) cands.add(b);
|
|
97
|
-
let rootJs = [];
|
|
98
|
-
try { rootJs = readdirSync(dir).filter((f) => /\.(c|m)?js$/.test(f)).slice(0, 6); } catch { /* gone */ }
|
|
99
|
-
for (const f of rootJs) cands.add(f);
|
|
100
|
-
if (!cands.size) cands.add("index.js");
|
|
101
|
-
for (const rel of cands) {
|
|
102
|
-
if (seenFiles >= capFiles) break;
|
|
103
|
-
const full = join(dir, String(rel));
|
|
104
|
-
try {
|
|
105
|
-
if (!existsSync(full) || statSync(full).size > 2_000_000) continue;
|
|
106
|
-
seenFiles++;
|
|
107
|
-
const text = readFileSync(full, "utf8");
|
|
108
|
-
for (const rule of classifyContent(text)) {
|
|
109
|
-
for (const at of matchRuleOffsets(rule, text)) {
|
|
110
|
-
// snippet keeps up to 60 chars of same-line PRE-context (like the rg path) so the
|
|
111
|
-
// comment/doc-context noise filter can see "// see" markers before the match
|
|
112
|
-
const lineStart = text.lastIndexOf("\n", at) + 1;
|
|
113
|
-
const nl = text.indexOf("\n", at);
|
|
114
|
-
const snippet = text.slice(Math.max(lineStart, at - 60), Math.min(nl < 0 ? text.length : nl, at + 200)).trim().slice(0, 240);
|
|
115
|
-
hits.push({ pkg, file: full, line: at >= 0 ? text.slice(0, at).split("\n").length : 0, text: snippet, rule });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} catch { /* unreadable */ }
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return hits;
|
|
122
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { makeFinding } from "../analysis/findings.js";
|
|
2
|
-
import { MALWARE_ALLOWLIST } from "./registry-sig.js";
|
|
3
|
-
import { classifyTyposquat } from "./typosquat.js";
|
|
4
|
-
|
|
5
|
-
const STRONG_URL_KEYS = new Set(["exfil-url", "exfil-ip", "cloud-metadata-url", "network-url"]);
|
|
6
|
-
|
|
7
|
-
function addTyposquatCoSignals(byPkg) {
|
|
8
|
-
for (const pkg of [...byPkg.keys()]) {
|
|
9
|
-
if (!byPkg.get(pkg).some((s) => !s.weak)) continue;
|
|
10
|
-
const sq = classifyTyposquat(pkg);
|
|
11
|
-
if (!sq) continue;
|
|
12
|
-
byPkg.get(pkg).push({
|
|
13
|
-
key: "typosquat",
|
|
14
|
-
severity: "medium",
|
|
15
|
-
nearZeroFp: false,
|
|
16
|
-
what: `name resembles "${sq.nearest}" (edit distance ${sq.distance})`,
|
|
17
|
-
snippet: `${pkg} ~= ${sq.nearest}`,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function visibleSignals(signals, allow) {
|
|
23
|
-
const kept = signals.filter((s) => !(allow && s.noisy));
|
|
24
|
-
if (!kept.length) return [];
|
|
25
|
-
// A literal URL, or even a network call to one, is ordinary package behavior without a second
|
|
26
|
-
// security signal. Keep those weak observations available as co-evidence (for example beside
|
|
27
|
-
// environment harvesting), but never create a standalone malware finding from them.
|
|
28
|
-
if (!kept.some((s) => !s.weak)) return [];
|
|
29
|
-
const hasStrongerUrl = kept.some((s) => STRONG_URL_KEYS.has(s.key));
|
|
30
|
-
return hasStrongerUrl ? kept.filter((s) => s.key !== "hardcoded-url") : kept;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Heuristic source matches can justify urgent review, but they cannot prove that code executed or
|
|
34
|
-
// credentials left the machine. Reserve `critical` for independently confirmed advisories/runtime
|
|
35
|
-
// evidence; this scanner therefore has an explicit HIGH ceiling.
|
|
36
|
-
function heuristicSeverity(baseSeverity, smoking, multi) {
|
|
37
|
-
if (baseSeverity === "critical") return "high";
|
|
38
|
-
if ((smoking || multi) && (baseSeverity === "medium" || baseSeverity === "low")) return "high";
|
|
39
|
-
return baseSeverity;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function evidenceRows(signals) {
|
|
43
|
-
const seen = new Set();
|
|
44
|
-
const rows = [];
|
|
45
|
-
for (const s of signals) {
|
|
46
|
-
const row = {
|
|
47
|
-
file: String(s.file).replace(/\\/g, "/"),
|
|
48
|
-
line: s.line || 0,
|
|
49
|
-
snippet: s.snippet.slice(0, 200),
|
|
50
|
-
};
|
|
51
|
-
const key = `${row.file}\0${row.line}\0${row.snippet}`;
|
|
52
|
-
if (seen.has(key)) continue;
|
|
53
|
-
seen.add(key);
|
|
54
|
-
rows.push(row);
|
|
55
|
-
if (rows.length >= 6) break;
|
|
56
|
-
}
|
|
57
|
-
return rows;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function buildMalwareFindings({ byPkg, importedPkgs, contentRoots, scanMode, excludedSignals }) {
|
|
61
|
-
addTyposquatCoSignals(byPkg);
|
|
62
|
-
const findings = [];
|
|
63
|
-
for (const [pkg, signals] of byPkg) {
|
|
64
|
-
const visible = visibleSignals(signals, MALWARE_ALLOWLIST.has(pkg));
|
|
65
|
-
if (!visible.length) continue;
|
|
66
|
-
const keys = new Set(visible.map((s) => s.key));
|
|
67
|
-
const smoking = visible.some((s) => s.nearZeroFp);
|
|
68
|
-
const escalationKeys = new Set(visible.filter((s) => !s.weak).map((s) => s.key));
|
|
69
|
-
const multi = escalationKeys.size >= 2;
|
|
70
|
-
for (const key of keys) {
|
|
71
|
-
const ofKey = visible.filter((s) => s.key === key);
|
|
72
|
-
const base = ofKey[0];
|
|
73
|
-
const severity = heuristicSeverity(base.severity, smoking, multi);
|
|
74
|
-
findings.push(makeFinding({
|
|
75
|
-
category: "malware",
|
|
76
|
-
rule: key,
|
|
77
|
-
severity,
|
|
78
|
-
confidence: base.nearZeroFp ? "high" : multi ? "medium" : "low",
|
|
79
|
-
assessment: "SUSPICIOUS_STATIC_EVIDENCE",
|
|
80
|
-
confirmedExecution: false,
|
|
81
|
-
requiresRuntimeConfirmation: true,
|
|
82
|
-
severityCeiling: "high",
|
|
83
|
-
title: `${base.what}: ${pkg}`,
|
|
84
|
-
detail: `${base.what} in installed package "${pkg}"${multi ? ` - co-occurs with ${[...keys].filter((k) => k !== key).join(", ")} (escalated)` : ""}${importedPkgs.has(pkg) ? ". This package IS imported by the repo's own code." : ". Transitive install (its install scripts may have run)."} Static heuristic evidence only: execution, package compromise, and credential exposure are NOT confirmed. Inspect the cited file and verify origin/lockfile/runtime evidence before acting.`,
|
|
85
|
-
package: pkg,
|
|
86
|
-
file: base.file ? String(base.file).replace(/\\/g, "/") : "",
|
|
87
|
-
line: base.line || 0,
|
|
88
|
-
evidence: evidenceRows(ofKey),
|
|
89
|
-
source: "heuristic",
|
|
90
|
-
verification: {
|
|
91
|
-
staticPattern: "MATCHED",
|
|
92
|
-
runtimeExecution: "NOT_VERIFIED",
|
|
93
|
-
packageOrigin: "NOT_VERIFIED",
|
|
94
|
-
lockfileIntegrity: "NOT_VERIFIED",
|
|
95
|
-
credentialExposure: "NOT_VERIFIED",
|
|
96
|
-
decision: "MANUAL_CONFIRMATION_REQUIRED",
|
|
97
|
-
},
|
|
98
|
-
fixHint: smoking
|
|
99
|
-
? `quarantine and inspect ${pkg}; verify package origin and lockfile integrity before removal; rotate secrets only if execution or credential exposure is confirmed`
|
|
100
|
-
: `review the cited code in ${pkg}`,
|
|
101
|
-
}));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return { findings, scanMode, packagesScanned: contentRoots.reduce((n, r) => n + (r.packages || 0), 0), excludedSignals };
|
|
105
|
-
}
|
package/src/security/match.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// Pure OSV advisory matcher — installed {ecosystem,name,version} × advisory affected[] → hits.
|
|
2
|
-
// From-scratch lite version comparator (semver-ish + PyPI epoch), NO deps. Exact `versions[]`
|
|
3
|
-
// membership is preferred (confidence high); range evaluation is the fallback (confidence medium) —
|
|
4
|
-
// per DEPS_SECURITY_PLAN P4: when unsure, label lower confidence rather than guessing "vulnerable".
|
|
5
|
-
export function parseVersion(v) {
|
|
6
|
-
let s = String(v || "").trim().replace(/^[v=]/, "");
|
|
7
|
-
let epoch = 0;
|
|
8
|
-
const em = s.match(/^(\d+)!(.*)$/); // PyPI epoch "1!2.0"
|
|
9
|
-
if (em) { epoch = Number(em[1]); s = em[2]; }
|
|
10
|
-
s = s.split("+")[0]; // build metadata never orders
|
|
11
|
-
const dash = s.indexOf("-");
|
|
12
|
-
const core = dash < 0 ? s : s.slice(0, dash);
|
|
13
|
-
const pre = dash < 0 ? [] : s.slice(dash + 1).split(".").filter(Boolean);
|
|
14
|
-
const nums = core.split(".").map((x) => { const n = parseInt(x, 10); return Number.isFinite(n) ? n : 0; });
|
|
15
|
-
return { epoch, nums, pre };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function compareVersions(a, b) {
|
|
19
|
-
const pa = parseVersion(a), pb = parseVersion(b);
|
|
20
|
-
if (pa.epoch !== pb.epoch) return pa.epoch - pb.epoch;
|
|
21
|
-
for (let i = 0; i < Math.max(pa.nums.length, pb.nums.length); i++) {
|
|
22
|
-
const x = pa.nums[i] || 0, y = pb.nums[i] || 0;
|
|
23
|
-
if (x !== y) return x - y;
|
|
24
|
-
}
|
|
25
|
-
if (!pa.pre.length && pb.pre.length) return 1; // release > its pre-release
|
|
26
|
-
if (pa.pre.length && !pb.pre.length) return -1;
|
|
27
|
-
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
28
|
-
const x = pa.pre[i], y = pb.pre[i];
|
|
29
|
-
if (x === undefined) return -1;
|
|
30
|
-
if (y === undefined) return 1;
|
|
31
|
-
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
|
|
32
|
-
if (nx && ny) { if (Number(x) !== Number(y)) return Number(x) - Number(y); }
|
|
33
|
-
else if (nx !== ny) return nx ? -1 : 1; // numeric identifiers order before alphanumeric (semver)
|
|
34
|
-
else { const c = x < y ? -1 : x > y ? 1 : 0; if (c) return c; }
|
|
35
|
-
}
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// affected = one OSV `affected[]` entry (already filtered to this package): {versions?, ranges?}.
|
|
40
|
-
// Returns { hit, by: "versions"|"range"|"range-open", confidence } — GIT ranges are skipped (commit
|
|
41
|
-
// hashes aren't comparable to release versions).
|
|
42
|
-
export function isVersionAffected(version, affected = {}) {
|
|
43
|
-
if (Array.isArray(affected.versions) && affected.versions.length) {
|
|
44
|
-
const norm = String(version).replace(/^v/, "");
|
|
45
|
-
if (affected.versions.some((x) => String(x).replace(/^v/, "") === norm)) return { hit: true, by: "versions", confidence: "high" };
|
|
46
|
-
// NOT a short-circuit: OSV's enumerated versions[] is often incomplete while ranges[] stays
|
|
47
|
-
// authoritative (that's what the server-side querybatch matches on). Falling through to the
|
|
48
|
-
// ranges below is what stops "querybatch flagged it, but re-analysis shows 0 vulnerabilities".
|
|
49
|
-
}
|
|
50
|
-
for (const r of affected.ranges || []) {
|
|
51
|
-
if (!r || r.type === "GIT") continue;
|
|
52
|
-
let active = false;
|
|
53
|
-
for (const e of r.events || []) { // OSV spec: events are sorted
|
|
54
|
-
if (e.introduced !== undefined) {
|
|
55
|
-
active = e.introduced === "0" || compareVersions(version, e.introduced) >= 0;
|
|
56
|
-
} else if (e.fixed !== undefined) {
|
|
57
|
-
if (active && compareVersions(version, e.fixed) < 0) return { hit: true, by: "range", confidence: "medium" };
|
|
58
|
-
active = false;
|
|
59
|
-
} else if (e.last_affected !== undefined) {
|
|
60
|
-
if (active && compareVersions(version, e.last_affected) <= 0) return { hit: true, by: "range", confidence: "medium" };
|
|
61
|
-
active = false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (active) return { hit: true, by: "range-open", confidence: "medium" }; // introduced, never fixed
|
|
65
|
-
}
|
|
66
|
-
return { hit: false };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// installed: [{ecosystem,name,version,...}]; queryFn(ecosystem, name) → normalized advisory rows
|
|
70
|
-
// [{id, kind: "vuln"|"malicious", severity, summary, url, affected}]. Dedup by (advisory id, package).
|
|
71
|
-
export function matchAdvisories(installed, queryFn) {
|
|
72
|
-
const hits = [];
|
|
73
|
-
const seen = new Set();
|
|
74
|
-
for (const pkg of installed || []) {
|
|
75
|
-
for (const adv of queryFn(pkg.ecosystem, pkg.name) || []) {
|
|
76
|
-
const key = `${adv.id}|${pkg.name}|${pkg.version}`;
|
|
77
|
-
if (seen.has(key)) continue;
|
|
78
|
-
const m = isVersionAffected(pkg.version, adv.affected || {});
|
|
79
|
-
if (!m.hit) continue;
|
|
80
|
-
seen.add(key);
|
|
81
|
-
hits.push({ pkg, adv, matchedBy: m.by, confidence: m.confidence });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return hits;
|
|
85
|
-
}
|