weavatrix 0.2.13 → 0.2.14

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.
Files changed (130) hide show
  1. package/README.md +83 -39
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.14.md +93 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +57 -38
  6. package/src/analysis/architecture/contract-graph.js +119 -0
  7. package/src/analysis/architecture/contract-schema.js +110 -0
  8. package/src/analysis/architecture/contract-storage.js +35 -0
  9. package/src/analysis/architecture/contract-verification.js +168 -0
  10. package/src/analysis/architecture-contract.js +16 -343
  11. package/src/analysis/change-classification/diff-parser.js +103 -0
  12. package/src/analysis/change-classification/options.js +40 -0
  13. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  14. package/src/analysis/change-classification.js +120 -519
  15. package/src/analysis/dead-code-review/policy.js +23 -0
  16. package/src/analysis/dead-code-review.js +9 -19
  17. package/src/analysis/dep-check.js +10 -57
  18. package/src/analysis/dep-rules.js +10 -303
  19. package/src/analysis/dependency/scoped-dependencies.js +40 -0
  20. package/src/analysis/endpoints/common.js +62 -0
  21. package/src/analysis/endpoints/extract.js +107 -0
  22. package/src/analysis/endpoints/inventory.js +84 -0
  23. package/src/analysis/endpoints/mounts.js +129 -0
  24. package/src/analysis/endpoints-java.js +80 -3
  25. package/src/analysis/endpoints.js +3 -448
  26. package/src/analysis/git-history/analytics.js +177 -0
  27. package/src/analysis/git-history/collector.js +109 -0
  28. package/src/analysis/git-history/options.js +68 -0
  29. package/src/analysis/git-history.js +128 -577
  30. package/src/analysis/health-capabilities.js +82 -0
  31. package/src/analysis/http-contracts/analysis.js +169 -0
  32. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  33. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  34. package/src/analysis/http-contracts/graph-context.js +143 -0
  35. package/src/analysis/http-contracts/matching.js +61 -0
  36. package/src/analysis/http-contracts/shared.js +86 -0
  37. package/src/analysis/http-contracts.js +6 -822
  38. package/src/analysis/internal-audit/dependency-health.js +111 -0
  39. package/src/analysis/internal-audit/python-manifests.js +65 -0
  40. package/src/analysis/internal-audit/repo-files.js +55 -0
  41. package/src/analysis/internal-audit/supply-chain.js +132 -0
  42. package/src/analysis/internal-audit.collect.js +5 -106
  43. package/src/analysis/internal-audit.run.js +113 -200
  44. package/src/analysis/jvm-dependency-evidence.js +69 -0
  45. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  46. package/src/analysis/source-correctness.js +225 -0
  47. package/src/analysis/structure/dependency-graph.js +156 -0
  48. package/src/analysis/structure/findings.js +142 -0
  49. package/src/graph/builder/go-receiver-resolution.js +112 -0
  50. package/src/graph/builder/js/queries.js +48 -0
  51. package/src/graph/builder/lang-go.js +89 -4
  52. package/src/graph/builder/lang-js.js +4 -44
  53. package/src/graph/builder/pass2-resolution.js +68 -0
  54. package/src/graph/freshness-probe.js +2 -1
  55. package/src/graph/incremental-refresh.js +3 -2
  56. package/src/graph/internal-builder.build.js +22 -183
  57. package/src/graph/internal-builder.pass2.js +161 -0
  58. package/src/graph/internal-builder.resolvers.js +2 -105
  59. package/src/graph/resolvers/rust.js +117 -0
  60. package/src/mcp/actions/advisories.mjs +18 -0
  61. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  62. package/src/mcp/actions/graph-sync.mjs +195 -0
  63. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  64. package/src/mcp/architecture-bootstrap.mjs +168 -0
  65. package/src/mcp/architecture-starter.mjs +234 -0
  66. package/src/mcp/catalog.mjs +20 -6
  67. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  68. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  69. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  70. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  71. package/src/mcp/graph/context-core.mjs +111 -0
  72. package/src/mcp/graph/context-seeds.mjs +208 -0
  73. package/src/mcp/graph/context-state.mjs +86 -0
  74. package/src/mcp/graph/tools-core.mjs +143 -0
  75. package/src/mcp/graph/tools-query.mjs +223 -0
  76. package/src/mcp/graph-context.mjs +4 -496
  77. package/src/mcp/health/audit-format.mjs +172 -0
  78. package/src/mcp/health/audit.mjs +171 -0
  79. package/src/mcp/health/dead-code.mjs +110 -0
  80. package/src/mcp/health/duplicates.mjs +83 -0
  81. package/src/mcp/health/endpoints.mjs +42 -0
  82. package/src/mcp/health/structure.mjs +219 -0
  83. package/src/mcp/runtime-version.mjs +32 -0
  84. package/src/mcp/server/auto-refresh.mjs +66 -0
  85. package/src/mcp/server/runtime-config.mjs +43 -0
  86. package/src/mcp/server/shutdown.mjs +40 -0
  87. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  88. package/src/mcp/sync/evidence-common.mjs +88 -0
  89. package/src/mcp/sync/evidence-duplicates.mjs +100 -0
  90. package/src/mcp/sync/evidence-health.mjs +56 -0
  91. package/src/mcp/sync/evidence-packages.mjs +95 -0
  92. package/src/mcp/sync/payload-common.mjs +97 -0
  93. package/src/mcp/sync/payload-v2.mjs +53 -0
  94. package/src/mcp/sync/payload-v3.mjs +79 -0
  95. package/src/mcp/sync-evidence.mjs +7 -402
  96. package/src/mcp/sync-payload.mjs +10 -244
  97. package/src/mcp/tools-actions.mjs +18 -414
  98. package/src/mcp/tools-architecture.mjs +18 -70
  99. package/src/mcp/tools-context.mjs +56 -15
  100. package/src/mcp/tools-endpoints.mjs +4 -1
  101. package/src/mcp/tools-graph.mjs +17 -331
  102. package/src/mcp/tools-health.mjs +24 -705
  103. package/src/mcp-server.mjs +35 -146
  104. package/src/precision/lsp-client/constants.js +12 -0
  105. package/src/precision/lsp-client/environment.js +20 -0
  106. package/src/precision/lsp-client/errors.js +15 -0
  107. package/src/precision/lsp-client/lifecycle.js +127 -0
  108. package/src/precision/lsp-client/message-parser.js +81 -0
  109. package/src/precision/lsp-client/protocol.js +212 -0
  110. package/src/precision/lsp-client/registry.js +58 -0
  111. package/src/precision/lsp-client/repo-uri.js +60 -0
  112. package/src/precision/lsp-client/stdio-client.js +151 -0
  113. package/src/precision/lsp-client.js +10 -682
  114. package/src/precision/lsp-overlay/build.js +238 -0
  115. package/src/precision/lsp-overlay/contract.js +61 -0
  116. package/src/precision/lsp-overlay/reference-results.js +108 -0
  117. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  118. package/src/precision/lsp-overlay/source-session.js +134 -0
  119. package/src/precision/lsp-overlay/store.js +150 -0
  120. package/src/precision/lsp-overlay/target-index.js +147 -0
  121. package/src/precision/lsp-overlay/target-query.js +55 -0
  122. package/src/precision/lsp-overlay.js +11 -868
  123. package/src/precision/typescript-lsp-provider.js +12 -682
  124. package/src/precision/typescript-provider/client.js +69 -0
  125. package/src/precision/typescript-provider/discovery.js +124 -0
  126. package/src/precision/typescript-provider/project-host.js +249 -0
  127. package/src/precision/typescript-provider/project-safety.js +161 -0
  128. package/src/precision/typescript-provider/reference-usage.js +53 -0
  129. package/src/version.js +7 -0
  130. package/docs/releases/v0.2.13.md +0 -48
@@ -0,0 +1,111 @@
1
+ import { readCoverageForRepo } from "../coverage-reports.js";
2
+ import { buildHealthCapabilityMatrix } from "../health-capabilities.js";
3
+
4
+ export function buildDependencyHealth({
5
+ repoPath,
6
+ graph,
7
+ repoFiles,
8
+ pyManifest,
9
+ dep,
10
+ goDep,
11
+ pyDep,
12
+ jvmDependencies,
13
+ externalImports,
14
+ findings,
15
+ packageScopes,
16
+ sourceFiles,
17
+ correctnessCoverage,
18
+ checks,
19
+ }) {
20
+ const dependencyFindings = findings.filter((finding) => ["unused-dep", "missing-dep", "duplicate-dep"].includes(finding.rule));
21
+ const graphComplete = !((graph.graphBuildMode && graph.graphBuildMode !== "full") || graph.graphBuildScope);
22
+ const npmManifests = repoFiles.filter((file) => /(^|\/)package\.json$/i.test(file));
23
+ const goManifests = repoFiles.filter((file) => /(^|\/)go\.mod$/i.test(file));
24
+ const pythonManifests = [...new Set((pyManifest.scopes || []).flatMap((scope) => scope.manifests || []))];
25
+ const ecosystems = {
26
+ npm: {
27
+ ecosystem: "npm",
28
+ present: npmManifests.length > 0,
29
+ status: npmManifests.length ? "CHECKED" : "NOT_PRESENT",
30
+ completeness: graphComplete ? "COMPLETE" : "PARTIAL",
31
+ manifests: npmManifests,
32
+ declared: dep.declared.size,
33
+ reason: npmManifests.length
34
+ ? "package.json declarations were compared with indexed JavaScript/TypeScript imports, including per-finding manifest/source/config evidence."
35
+ : "No package.json was discovered.",
36
+ },
37
+ go: {
38
+ ecosystem: "go",
39
+ present: goManifests.length > 0,
40
+ status: goManifests.length ? "CHECKED" : "NOT_PRESENT",
41
+ completeness: goManifests.length ? "PARTIAL" : "NOT_APPLICABLE",
42
+ manifests: goManifests,
43
+ declared: goDep.declared.size,
44
+ reason: goManifests.length
45
+ ? "Go dependency checks use indexed imports and the root go.mod; nested modules and build-tag-specific resolution are not fully modeled."
46
+ : "No go.mod was discovered.",
47
+ },
48
+ python: {
49
+ ecosystem: "python",
50
+ present: pyManifest.present,
51
+ status: pyManifest.present ? "CHECKED" : "NOT_PRESENT",
52
+ completeness: pyManifest.present ? "PARTIAL" : "NOT_APPLICABLE",
53
+ manifests: pythonManifests,
54
+ declared: pyDep.declared.size,
55
+ reason: pyManifest.present
56
+ ? "Python declarations were compared with indexed imports; environment markers, extras and dynamic imports remain bounded static evidence."
57
+ : "No supported Python dependency manifest was discovered.",
58
+ },
59
+ maven: { ecosystem: "maven", ...jvmDependencies.maven },
60
+ gradle: { ecosystem: "gradle", ...jvmDependencies.gradle },
61
+ };
62
+ const present = Object.values(ecosystems).filter((item) => item.present);
63
+ const status = present.length === 0
64
+ ? "NOT_CHECKED"
65
+ : graphComplete && present.every((item) => item.status === "CHECKED" && item.completeness === "COMPLETE")
66
+ ? "COMPLETE"
67
+ : "PARTIAL";
68
+ const importedPackages = new Set(externalImports
69
+ .filter((entry) => entry?.pkg && !entry.builtin && !entry.unresolved)
70
+ .map((entry) => `${entry.ecosystem || (/\.java$/i.test(entry.file || "") ? "maven-unresolved" : "npm")}:${entry.pkg}`));
71
+ const supportedDeclared = dep.declared.size + goDep.declared.size + pyDep.declared.size;
72
+ const jvmDeclared = jvmDependencies.maven.declared + jvmDependencies.gradle.declared;
73
+ const measuredCoverage = readCoverageForRepo(repoPath, sourceFiles);
74
+ const healthCapabilities = buildHealthCapabilityMatrix({
75
+ graphComplete,
76
+ dependencyStatus: status,
77
+ dependencyEcosystems: ecosystems,
78
+ checks,
79
+ sourceFiles,
80
+ correctnessCoverage,
81
+ measuredCoverageFiles: measuredCoverage.size,
82
+ });
83
+ return {
84
+ manifestDeps: supportedDeclared + jvmDeclared,
85
+ healthCapabilities,
86
+ dependencyReport: {
87
+ status,
88
+ evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
89
+ perFindingVerification: present.some((item) => item.status === "CHECKED")
90
+ && dependencyFindings.every((finding) => finding.verification != null),
91
+ verificationCoverage: Object.fromEntries(present.map((item) => [item.ecosystem, `${item.status}/${item.completeness}`])),
92
+ ecosystems,
93
+ declared: supportedDeclared + jvmDeclared,
94
+ importedPackages: importedPackages.size,
95
+ importRecords: externalImports.length,
96
+ unused: dependencyFindings.filter((finding) => finding.rule === "unused-dep").length,
97
+ missing: dependencyFindings.filter((finding) => finding.rule === "missing-dep").length,
98
+ duplicateDeclarations: dependencyFindings.filter((finding) => finding.rule === "duplicate-dep").length,
99
+ unusedRequiringReview: dependencyFindings.filter((finding) => finding.rule === "unused-dep" && finding.verification?.decision === "REVIEW_REQUIRED").length,
100
+ missingWithSourceEvidence: dependencyFindings.filter((finding) => finding.rule === "missing-dep" && finding.verification?.indexedSourceImports?.status === "FOUND").length,
101
+ packageScopes: packageScopes.length,
102
+ reason: status === "NOT_CHECKED"
103
+ ? "No dependency manifest was discovered, so manifest-to-import verification did not run and no dependency verdict was produced."
104
+ : status === "COMPLETE"
105
+ ? "Every discovered dependency ecosystem has complete supported manifest-to-import evidence for the indexed repository."
106
+ : present.some((item) => item.status === "NOT_SUPPORTED")
107
+ ? `Dependency evidence is PARTIAL: ${present.filter((item) => item.status === "NOT_SUPPORTED").map((item) => item.ecosystem).join(", ")} manifests were counted, but package-to-artifact verification is NOT_SUPPORTED.`
108
+ : "Dependency checks ran at their documented evidence level, but at least one graph or ecosystem surface is partial; this is not a repository-wide clean bill.",
109
+ },
110
+ };
111
+ }
@@ -0,0 +1,65 @@
1
+ import {dirname} from 'node:path'
2
+ import {createRepoBoundary} from '../../repo-path.js'
3
+ import {parseRequirementsNames, parsePyprojectDeps, parsePipfileDeps, pep503} from '../manifests.js'
4
+ import {listRepoFiles, readRepoText} from './repo-files.js'
5
+
6
+ const normalizeRoot = (root) => {
7
+ const value = String(root || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '')
8
+ return value === '.' ? '' : value
9
+ }
10
+
11
+ // Python dependencies are owned by the nearest manifest root, including nested services.
12
+ export function collectPyManifest(repoRoot) {
13
+ const boundary = createRepoBoundary(repoRoot)
14
+ const scopes = new Map()
15
+ const scopeFor = (root) => {
16
+ const normalized = normalizeRoot(root)
17
+ if (!scopes.has(normalized)) scopes.set(normalized, {root: normalized, present: false, deps: [], manifests: []})
18
+ return scopes.get(normalized)
19
+ }
20
+ const addManifest = (root, manifest, parsedDeps, present = true) => {
21
+ if (!present) return
22
+ const scope = scopeFor(root)
23
+ scope.present = true
24
+ scope.manifests.push(manifest)
25
+ scope.deps.push(...parsedDeps.map((dep) => ({...dep, manifest})))
26
+ }
27
+ const files = listRepoFiles(repoRoot)
28
+ for (const file of files.filter((name) => /(^|\/)requirements[\w.-]*\.(?:txt|in)$/i.test(name)
29
+ || /(^|\/)requirements\/[^/]+\.(?:txt|in)$/i.test(name))) {
30
+ const text = readRepoText(boundary, file)
31
+ if (text == null) continue
32
+ const parent = normalizeRoot(dirname(file))
33
+ const root = /(^|\/)requirements$/i.test(parent) ? normalizeRoot(dirname(parent)) : parent
34
+ const dev = /dev|test|lint|doc|ci/i.test(file.slice(file.lastIndexOf('/') + 1))
35
+ addManifest(root, file, parseRequirementsNames(text).map((dep) => ({...dep, dev})))
36
+ }
37
+ for (const file of files.filter((name) => /(^|\/)pyproject\.toml$/i.test(name))) {
38
+ const parsed = parsePyprojectDeps(readRepoText(boundary, file))
39
+ addManifest(dirname(file), file, parsed.deps, parsed.present)
40
+ }
41
+ for (const file of files.filter((name) => /(^|\/)Pipfile$/i.test(name))) {
42
+ const parsed = parsePipfileDeps(readRepoText(boundary, file))
43
+ addManifest(dirname(file), file, parsed.deps, parsed.present)
44
+ }
45
+ const normalizedScopes = [...scopes.values()]
46
+ .map((scope) => {
47
+ const seen = new Set()
48
+ return {
49
+ ...scope,
50
+ manifests: [...new Set(scope.manifests)].sort(),
51
+ deps: scope.deps.filter((dep) => {
52
+ const key = pep503(dep.name)
53
+ if (!key || seen.has(key)) return false
54
+ seen.add(key)
55
+ return true
56
+ }),
57
+ }
58
+ })
59
+ .sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root))
60
+ return {
61
+ present: normalizedScopes.some((scope) => scope.present),
62
+ deps: normalizedScopes.flatMap((scope) => scope.deps),
63
+ scopes: normalizedScopes,
64
+ }
65
+ }
@@ -0,0 +1,55 @@
1
+ import {readFileSync, readdirSync} from 'node:fs'
2
+ import {join} from 'node:path'
3
+ import {spawnSync} from 'node:child_process'
4
+ import {childProcessEnv} from '../../child-env.js'
5
+ import {filterWeavatrixIgnored} from '../../path-ignore.js'
6
+
7
+ export const readText = (path) => {
8
+ try { return readFileSync(path, 'utf8') } catch { return null }
9
+ }
10
+
11
+ export const readJson = (path) => {
12
+ try { return JSON.parse(readFileSync(path, 'utf8')) } catch { return null }
13
+ }
14
+
15
+ export const readRepoText = (boundary, relativePath) => {
16
+ const resolved = boundary.resolve(relativePath)
17
+ return resolved.ok ? readText(resolved.path) : null
18
+ }
19
+
20
+ export const readRepoJson = (boundary, relativePath) => {
21
+ const resolved = boundary.resolve(relativePath)
22
+ return resolved.ok ? readJson(resolved.path) : null
23
+ }
24
+
25
+ const SKIP_DIRS = new Set([
26
+ '.git', '.hg', '.svn', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', 'out',
27
+ 'release', 'weavatrix-graphs', '__pycache__', '.venv', 'venv', 'env', '.tox', 'site-packages',
28
+ '.mypy_cache', '.pytest_cache',
29
+ ])
30
+
31
+ export function listRepoFiles(repoRoot) {
32
+ try {
33
+ const result = spawnSync('git', ['-C', repoRoot, 'ls-files', '--cached', '--others', '--exclude-standard', '-z'], {
34
+ encoding: 'utf8', windowsHide: true, timeout: 15_000, maxBuffer: 32 * 1024 * 1024,
35
+ env: childProcessEnv(),
36
+ })
37
+ if (result.status === 0) {
38
+ const files = String(result.stdout || '').split('\0').filter(Boolean).map((file) => file.replace(/\\/g, '/'))
39
+ return filterWeavatrixIgnored(repoRoot, files)
40
+ }
41
+ } catch { /* non-Git repo or git unavailable: use the bounded walker below */ }
42
+
43
+ const files = []
44
+ const walk = (absolute, parts = []) => {
45
+ let entries = []
46
+ try { entries = readdirSync(absolute, {withFileTypes: true}) } catch { return }
47
+ for (const entry of entries) {
48
+ if (entry.isDirectory()) {
49
+ if (!SKIP_DIRS.has(entry.name)) walk(join(absolute, entry.name), [...parts, entry.name])
50
+ } else if (entry.isFile()) files.push([...parts, entry.name].join('/'))
51
+ }
52
+ }
53
+ walk(repoRoot)
54
+ return filterWeavatrixIgnored(repoRoot, files)
55
+ }
@@ -0,0 +1,132 @@
1
+ import { collectInstalled } from "../../security/installed.js";
2
+ import { advisoryQueryFingerprint, loadStore, queryStore } from "../../security/advisory-store.js";
3
+ import { matchAdvisories } from "../../security/match.js";
4
+ import { scanMalware } from "../../security/malware-heuristics.js";
5
+ import { classifyTyposquat } from "../../security/typosquat.js";
6
+ import { makeFinding } from "../findings.js";
7
+ import { packageReachability } from "../package-reachability.js";
8
+
9
+ const LOWER_SEVERITY = { critical: "high", high: "medium", medium: "low", low: "info", info: "info" };
10
+
11
+ const removalHint = (pkg) => {
12
+ if (pkg?.ecosystem === "npm") return `npm uninstall ${pkg.name} and audit what it touched`;
13
+ if (pkg?.ecosystem === "PyPI") return `remove ${pkg.name} from the Python environment/manifest and audit what it touched`;
14
+ if (pkg?.ecosystem === "Go") return `remove ${pkg.name} from the Go module graph, run go mod tidy, and audit what it touched`;
15
+ return `remove ${pkg?.name || "the package"} with its ecosystem's package manager and audit what it touched`;
16
+ };
17
+
18
+ export async function runSupplyChainChecks(repoPath, {
19
+ externalImports = [],
20
+ packageScopes = [],
21
+ isNonProductPath = () => false,
22
+ advisoryStorePath,
23
+ skipMalwareScan = false,
24
+ malwareExclusions = {},
25
+ rgPath = "",
26
+ } = {}) {
27
+ const findings = [];
28
+ let advisoryDbDate = null;
29
+ let installedCount = 0;
30
+ let inst = { installed: [], drift: [] };
31
+ const checks = {
32
+ osv: { status: "NOT_CHECKED", detail: "Advisory cache was never refreshed for this repository. Enable the osv profile (or advisories capability), then call refresh_advisories explicitly to opt in to sending pinned package names and versions to OSV.dev." },
33
+ malware: { status: skipMalwareScan ? "NOT_CHECKED" : "PENDING", detail: skipMalwareScan ? "Installed-package malware scan is opt-in and was not requested." : "" },
34
+ };
35
+
36
+ try {
37
+ inst = collectInstalled(repoPath);
38
+ installedCount = inst.installed.length;
39
+ const store = advisoryStorePath ? loadStore(advisoryStorePath) : loadStore();
40
+ const repoStamp = store.meta?.repos?.[repoPath] || null;
41
+ advisoryDbDate = typeof repoStamp === "string" ? repoStamp : repoStamp?.fetched_at || null;
42
+ if (advisoryDbDate) {
43
+ let status = typeof repoStamp === "object" && ["OK", "PARTIAL", "ERROR"].includes(repoStamp.status) ? repoStamp.status : "PARTIAL";
44
+ const fingerprintMatches = typeof repoStamp === "object" && repoStamp.query_fingerprint === advisoryQueryFingerprint(inst.installed);
45
+ if (!fingerprintMatches && status === "OK") status = "PARTIAL";
46
+ const coverage = typeof repoStamp === "object" && Number.isFinite(repoStamp.queried)
47
+ ? ` (${repoStamp.queried_ok ?? repoStamp.queried}/${repoStamp.queried} package versions queried successfully)`
48
+ : "";
49
+ 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.";
50
+ checks.osv = {
51
+ status,
52
+ detail: `${status === "PARTIAL" ? "Partially matched" : "Matched"} installed packages against the cached OSV snapshot from ${advisoryDbDate}${coverage}.${drift}`,
53
+ checkedAt: advisoryDbDate,
54
+ };
55
+ for (const hit of matchAdvisories(inst.installed, (ecosystem, name) => queryStore(store, ecosystem, name))) {
56
+ const malicious = hit.adv.kind === "malicious";
57
+ const reachability = packageReachability(externalImports, hit.pkg.name, { isNonProductPath });
58
+ const observedInProduct = reachability.state === "DIRECT_RUNTIME_IMPORT";
59
+ const reachabilityDetail = observedInProduct
60
+ ? ` Graph reachability: directly imported by product code in ${reachability.directRuntimeImports} callsite(s) across ${reachability.files.length} file(s).`
61
+ : ` Graph reachability: ${reachability.state}; ${reachability.note}`;
62
+ findings.push(makeFinding({
63
+ category: malicious ? "malware" : "vulnerability",
64
+ rule: malicious ? "malicious-package" : "known-vuln",
65
+ severity: malicious || observedInProduct ? (malicious ? "critical" : hit.adv.severity) : LOWER_SEVERITY[hit.adv.severity] || hit.adv.severity,
66
+ confidence: malicious || observedInProduct ? hit.confidence : "low",
67
+ title: `${malicious ? "Known-malicious package" : `Known vulnerability (${hit.adv.id})`}: ${hit.pkg.name}@${hit.pkg.version}`,
68
+ detail: `${hit.adv.summary || hit.adv.id}${hit.adv.fixedIn.length ? ` Fixed in: ${hit.adv.fixedIn.join(", ")}.` : malicious ? " Remove this package immediately and rotate any secrets it could reach." : ""} (matched by ${hit.matchedBy}${hit.adv.aliases.length ? `; aliases ${hit.adv.aliases.join(", ")}` : ""}).${reachabilityDetail}`,
69
+ package: hit.pkg.name,
70
+ version: hit.pkg.version,
71
+ reachability,
72
+ evidence: [
73
+ { file: hit.adv.url, line: 0, snippet: `installed via ${hit.pkg.source}${hit.pkg.dev ? " (dev)" : ""}` },
74
+ ...reachability.evidence.slice(0, 5).map((item) => ({ file: item.file, line: item.line, snippet: `${item.typeOnly ? "type-only " : ""}${item.kind}` })),
75
+ ],
76
+ source: "osv",
77
+ fixHint: malicious
78
+ ? removalHint(hit.pkg)
79
+ : hit.adv.fixedIn.length
80
+ ? `upgrade ${hit.pkg.name} to ${hit.adv.fixedIn.at(-1)}+ with its ecosystem's package manager`
81
+ : "no fixed version published; consider replacing the package",
82
+ }));
83
+ }
84
+ }
85
+ const directDependencyNames = new Set(packageScopes.flatMap((scope) => Object.keys({ ...(scope.pkg?.dependencies || {}), ...(scope.pkg?.devDependencies || {}) })));
86
+ for (const name of directDependencyNames) {
87
+ const candidate = classifyTyposquat(name);
88
+ if (!candidate) continue;
89
+ findings.push(makeFinding({
90
+ category: "malware",
91
+ rule: "typosquat",
92
+ severity: "medium",
93
+ confidence: "low",
94
+ title: `Possible typosquat: ${name} (looks like "${candidate.nearest}")`,
95
+ detail: `Direct dependency "${name}" is edit-distance ${candidate.distance} from the popular package "${candidate.nearest}". Confirm you meant "${name}" and not "${candidate.nearest}" — name-confusion is a common supply-chain lure.`,
96
+ package: name,
97
+ source: "internal",
98
+ fixHint: `verify "${name}" is the intended package (not a typo of "${candidate.nearest}")`,
99
+ }));
100
+ }
101
+ for (const item of inst.drift.slice(0, 20)) {
102
+ findings.push(makeFinding({
103
+ category: "malware",
104
+ rule: "lockfile-drift",
105
+ severity: "low",
106
+ confidence: "medium",
107
+ title: `Lockfile drift: ${item.name} (locked ${item.locked}, installed ${item.installed})`,
108
+ 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.",
109
+ package: item.name,
110
+ version: item.installed,
111
+ source: "internal",
112
+ fixHint: "npm ci (clean install from the lockfile)",
113
+ }));
114
+ }
115
+ } catch (error) {
116
+ checks.osv = { status: "ERROR", detail: `Offline advisory matching failed: ${error instanceof Error ? error.message : String(error)}` };
117
+ }
118
+
119
+ let malwareScan = null;
120
+ if (!skipMalwareScan) {
121
+ try {
122
+ const importedPkgs = new Set(externalImports.filter((entry) => entry.pkg && !entry.builtin).map((entry) => entry.pkg));
123
+ const scan = await scanMalware(repoPath, { installed: inst.installed, importedPkgs, malwareExclusions, rgPath });
124
+ findings.push(...scan.findings);
125
+ malwareScan = { scanMode: scan.scanMode, packagesScanned: scan.packagesScanned, findings: scan.findings.length, excludedSignals: scan.excludedSignals || 0 };
126
+ checks.malware = { status: "OK", detail: `Scanned ${scan.packagesScanned} installed package(s) using ${scan.scanMode}.` };
127
+ } catch (error) {
128
+ checks.malware = { status: "ERROR", detail: `Installed-package malware scan failed: ${error instanceof Error ? error.message : String(error)}` };
129
+ }
130
+ }
131
+ return { findings, checks, installedCount, advisoryDbDate, malwareScan };
132
+ }
@@ -1,55 +1,13 @@
1
1
  // internal-audit.collect.js — filesystem collection helpers for the internal audit: source/config
2
2
  // text gathering, workspace package names, and the Python manifest reader. Split from internal-audit.js.
3
- import { readFileSync, readdirSync } from "node:fs";
3
+ import { readdirSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
- import { spawnSync } from "node:child_process";
6
- import { parseRequirementsNames, parsePyprojectDeps, parsePipfileDeps, pep503 } from "./manifests.js";
7
5
  import { createRepoBoundary } from "../repo-path.js";
8
- import { childProcessEnv } from "../child-env.js";
9
- import { filterWeavatrixIgnored } from "../path-ignore.js";
6
+ import {listRepoFiles, readJson, readRepoJson, readRepoText, readText} from './internal-audit/repo-files.js'
10
7
 
11
- export const readText = (p) => { try { return readFileSync(p, "utf8"); } catch { return null; } };
12
- export const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
13
- export const readRepoText = (boundary, relativePath) => {
14
- const resolved = boundary.resolve(relativePath);
15
- return resolved.ok ? readText(resolved.path) : null;
16
- };
17
- export const readRepoJson = (boundary, relativePath) => {
18
- const resolved = boundary.resolve(relativePath);
19
- return resolved.ok ? readJson(resolved.path) : null;
20
- };
21
- const SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?|py|go|vue|svelte)$/i;
22
- const SOURCE_SKIP_DIRS = new Set([
23
- ".git", ".hg", ".svn", "node_modules", "vendor", "dist", "build", "coverage", ".next", "out",
24
- "release", "weavatrix-graphs", "__pycache__", ".venv", "venv", "env", ".tox", "site-packages",
25
- ".mypy_cache", ".pytest_cache",
26
- ]);
27
-
28
- // One file universe for every filesystem-backed audit. In Git repos this exactly matches tracked files
29
- // plus untracked/non-ignored work, so release bundles and other .gitignore outputs cannot re-enter through
30
- // text/config/manifest fallback collectors after the graph builder correctly omitted them.
31
- export function listRepoFiles(repoRoot) {
32
- try {
33
- const r = spawnSync("git", ["-C", repoRoot, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
34
- encoding: "utf8", windowsHide: true, timeout: 15_000, maxBuffer: 32 * 1024 * 1024,
35
- env: childProcessEnv(),
36
- });
37
- if (r.status === 0) return filterWeavatrixIgnored(repoRoot, String(r.stdout || "").split("\0").filter(Boolean).map((f) => f.replace(/\\/g, "/")));
38
- } catch { /* non-Git repo or git unavailable: use the bounded walker below */ }
39
-
40
- const files = [];
41
- const walk = (abs, parts = []) => {
42
- let entries = [];
43
- try { entries = readdirSync(abs, { withFileTypes: true }); } catch { return; }
44
- for (const entry of entries) {
45
- if (entry.isDirectory()) {
46
- if (!SOURCE_SKIP_DIRS.has(entry.name)) walk(join(abs, entry.name), [...parts, entry.name]);
47
- } else if (entry.isFile()) files.push([...parts, entry.name].join("/"));
48
- }
49
- };
50
- walk(repoRoot);
51
- return filterWeavatrixIgnored(repoRoot, files);
52
- }
8
+ export {listRepoFiles, readJson, readRepoJson, readRepoText, readText} from './internal-audit/repo-files.js'
9
+ export {collectPyManifest} from './internal-audit/python-manifests.js'
10
+ const SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|cs|vue|svelte)$/i;
53
11
 
54
12
  const NON_RUNTIME_DIR_RE = /^(?:templates?|examples?|samples?|fixtures?|snippets?|__fixtures__)$/i;
55
13
  const NON_RUNTIME_README_RE = /(?:\b(?:reusable|copyable|reference)\b[\s\S]{0,160}\b(?:templates?|snippets?|examples?|samples?)\b|\b(?:these|contents?)\s+are\s+templates?\b)/i;
@@ -253,62 +211,3 @@ export function workspacePkgNames(repoRoot, pkg) {
253
211
  }
254
212
 
255
213
  export const TEST_FILE_RE = /(^|[/])(test|tests|__tests__|spec|e2e|__mocks__)([/]|$)|[._-](test|spec)\.[a-z0-9]+$/i;
256
-
257
- // Python declared deps are owned by the nearest manifest root, matching nested service/monorepo
258
- // layouts. `requirements/*.txt` belongs to the directory above `requirements`; a colocated
259
- // requirements.txt, pyproject.toml or Pipfile owns its own directory.
260
- // present=false (no manifest at all) softens missing-dep findings instead of suppressing them.
261
- export function collectPyManifest(repoRoot) {
262
- const boundary = createRepoBoundary(repoRoot);
263
- const scopes = new Map();
264
- const scopeFor = (root) => {
265
- const normalized = normRoot(root);
266
- if (!scopes.has(normalized)) scopes.set(normalized, { root: normalized, present: false, deps: [], manifests: [] });
267
- return scopes.get(normalized);
268
- };
269
- const addManifest = (root, manifest, parsedDeps, present = true) => {
270
- if (!present) return;
271
- const scope = scopeFor(root);
272
- scope.present = true;
273
- scope.manifests.push(manifest);
274
- scope.deps.push(...parsedDeps.map((dep) => ({ ...dep, manifest })));
275
- };
276
- const files = listRepoFiles(repoRoot);
277
- for (const file of files.filter((name) => /(^|\/)requirements[\w.-]*\.(?:txt|in)$/i.test(name)
278
- || /(^|\/)requirements\/[^/]+\.(?:txt|in)$/i.test(name))) {
279
- const t = readRepoText(boundary, file);
280
- if (t == null) continue;
281
- const parent = normRoot(dirname(file));
282
- const root = /(^|\/)requirements$/i.test(parent) ? normRoot(dirname(parent)) : parent;
283
- const dev = /dev|test|lint|doc|ci/i.test(file.slice(file.lastIndexOf("/") + 1));
284
- addManifest(root, file, parseRequirementsNames(t).map((dep) => ({ ...dep, dev })));
285
- }
286
- for (const file of files.filter((name) => /(^|\/)pyproject\.toml$/i.test(name))) {
287
- const parsed = parsePyprojectDeps(readRepoText(boundary, file));
288
- addManifest(dirname(file), file, parsed.deps, parsed.present);
289
- }
290
- for (const file of files.filter((name) => /(^|\/)Pipfile$/i.test(name))) {
291
- const parsed = parsePipfileDeps(readRepoText(boundary, file));
292
- addManifest(dirname(file), file, parsed.deps, parsed.present);
293
- }
294
- const normalizedScopes = [...scopes.values()]
295
- .map((scope) => {
296
- const seen = new Set();
297
- return {
298
- ...scope,
299
- manifests: [...new Set(scope.manifests)].sort(),
300
- deps: scope.deps.filter((dep) => {
301
- const key = pep503(dep.name);
302
- if (!key || seen.has(key)) return false;
303
- seen.add(key);
304
- return true;
305
- }),
306
- };
307
- })
308
- .sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root));
309
- return {
310
- present: normalizedScopes.some((scope) => scope.present),
311
- deps: normalizedScopes.flatMap((scope) => scope.deps),
312
- scopes: normalizedScopes,
313
- };
314
- }