weavatrix 0.2.18 → 0.3.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/README.md +102 -93
- package/SECURITY.md +13 -31
- package/bin/weavatrix-mcp.mjs +2 -1
- package/docs/adr/0001-v0.3-offline-online-split.md +9 -7
- package/docs/releases/v0.2.19.md +33 -0
- package/docs/releases/v0.3.0.md +43 -0
- package/package.json +9 -3
- package/skill/SKILL.md +34 -59
- package/src/analysis/architecture/contract-storage.js +2 -2
- package/src/analysis/audit-extensions.js +60 -0
- package/src/analysis/duplicate-groups.js +13 -2
- package/src/analysis/duplicates.compute.js +7 -2
- package/src/analysis/health-capabilities.js +2 -1
- package/src/analysis/internal-audit/supply-chain.js +17 -2
- package/src/analysis/jvm-artifact-index.js +87 -0
- package/src/analysis/jvm-dependency-evidence.js +31 -8
- package/src/analysis/transport-contracts.js +78 -11
- package/src/analysis/transport-runtime-evidence.js +155 -0
- package/src/child-env.js +2 -2
- package/src/extension/local-services.mjs +75 -0
- package/src/graph/internal-builder.langs.js +2 -0
- package/src/graph/repo-registry.js +2 -2
- package/src/mcp/catalog.mjs +47 -21
- package/src/mcp/evidence/duplicate-member-order.mjs +1 -1
- package/src/mcp/evidence-snapshot.health.mjs +2 -2
- package/src/mcp/extension-api.mjs +77 -0
- package/src/mcp/health/audit-format.mjs +11 -1
- package/src/mcp/health/audit.mjs +10 -7
- package/src/mcp/health/duplicates.mjs +5 -3
- package/src/mcp/sync-payload.mjs +1 -1
- package/src/mcp/tools-actions.mjs +1 -8
- package/src/mcp/tools-architecture.mjs +2 -2
- package/src/mcp/tools-company.mjs +4 -2
- package/src/mcp-runtime.mjs +2 -0
- package/src/mcp-server.mjs +28 -17
- package/src/security/advisory-store.js +133 -181
- package/src/security/malware-scoring.js +4 -0
- package/src/security/registry-sig.rules.js +4 -3
- package/src/security/rust-advisory-report.js +60 -0
- package/docs/releases/v0.2.18.md +0 -41
- package/src/mcp/actions/advisories.mjs +0 -17
- package/src/mcp/actions/graph-sync.mjs +0 -195
- package/src/mcp/actions/hosted-architecture.mjs +0 -57
package/skill/SKILL.md
CHANGED
|
@@ -6,7 +6,7 @@ description: "Use the Weavatrix MCP as a reusable local repository-intelligence
|
|
|
6
6
|
# weavatrix MCP
|
|
7
7
|
|
|
8
8
|
Structure-analysis tools over a prebuilt code graph plus the weavatrix analysis engines. The default
|
|
9
|
-
`offline` profile
|
|
9
|
+
`offline` profile contains no HTTP tool and permits repository switching only through an
|
|
10
10
|
explicit local `open_repo` call. Use `pinned` when a shared MCP process must never change or inspect
|
|
11
11
|
outside its startup repository.
|
|
12
12
|
|
|
@@ -17,27 +17,21 @@ Tools are named `mcp__weavatrix__…`. If none are available, ask the user to re
|
|
|
17
17
|
`codex mcp add weavatrix -- npx -y weavatrix <repoRoot>`), then retry.
|
|
18
18
|
|
|
19
19
|
First compare the selected profile with the `graph_stats` runtime line. Expected catalogs are 31
|
|
20
|
-
tools for `pinned
|
|
20
|
+
tools for `pinned` and 34 for `offline`. Weavatrix refuses
|
|
21
21
|
`initialize`, `tools/list`, and tool calls when the running package version differs from the
|
|
22
22
|
`package.json` version on disk, with a loud `STALE_RUNTIME` error; restart/reconnect rather than using
|
|
23
23
|
an old daemon. `WEAVATRIX_ALLOW_STALE_RUNTIME=1` is only for deliberate source development. A custom
|
|
24
|
-
capability list needs `crossrepo` for `trace_api_contract
|
|
25
|
-
|
|
24
|
+
capability list needs `crossrepo` for `trace_api_contract`. Legacy network profile names fail loudly
|
|
25
|
+
and direct the user to the separate `weavatrix-online` package.
|
|
26
26
|
|
|
27
27
|
Profiles use the same npm package and binary:
|
|
28
28
|
|
|
29
29
|
- `offline` (default): all local analysis and explicit `open_repo`; no HTTP tools.
|
|
30
30
|
- `pinned`: local analysis with no `open_repo`, no global/cross-repository graph access, and no HTTP tools.
|
|
31
|
-
- `osv`: `offline` plus explicit `refresh_advisories`.
|
|
32
|
-
- `hosted` / `full`: `osv` plus local-only `preview_sync` and the explicitly invoked network tools
|
|
33
|
-
`pull_architecture_contract` and confirmed `sync_graph`.
|
|
34
|
-
|
|
35
|
-
The legacy `online` capability remains a compatibility alias for `advisories,hosted`; prefer the
|
|
36
|
-
named profiles for new registrations.
|
|
37
31
|
|
|
38
32
|
## Intent router
|
|
39
33
|
|
|
40
|
-
Weavatrix is not one report or three fixed workflows. Its
|
|
34
|
+
Weavatrix is not one report or three fixed workflows. Its 34 methods expose different bounded views
|
|
41
35
|
and analyses over the same reusable graph. Start from the task and choose the smallest sufficient
|
|
42
36
|
projection; expand only when the answer requires it.
|
|
43
37
|
|
|
@@ -95,20 +89,26 @@ projection; expand only when the answer requires it.
|
|
|
95
89
|
a changed file remains existing.
|
|
96
90
|
- **Review dependency declarations/imports**: `run_audit category=dependencies`; it includes missing,
|
|
97
91
|
unused, duplicate, unresolved-import and lockfile-drift evidence without relabelling identities.
|
|
98
|
-
Maven/Gradle
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
Maven/Gradle imports are mapped to exact artifact ownership when the referenced class is present in
|
|
93
|
+
an already installed local JAR. Missing artifacts, ambiguous class owners, version catalogs or
|
|
94
|
+
dynamic build logic remain `PARTIAL`/`NOT_SUPPORTED`, never a false clean `0 declared / 0 external`.
|
|
95
|
+
- **Review vulnerability evidence**: `run_audit category=vulnerability`. `NOT_CHECKED` is unknown,
|
|
96
|
+
never clean; remote refresh is an explicit `weavatrix-online` workflow.
|
|
97
|
+
- **Review dead files, functions, methods and symbols**: `find_dead_code`; every result remains a
|
|
98
|
+
review candidate with framework/dynamic/public-API caveats, never an auto-delete verdict.
|
|
99
|
+
- **Trace dynamic GraphQL/gRPC/Kafka contracts**: export a source-free
|
|
100
|
+
`weavatrix.transport-runtime.v1` JSON report (normalized observations or OTLP `resourceSpans`) to
|
|
101
|
+
`.weavatrix/transport-runtime.json`, rebuild the graph, and call `trace_api_contract`. Only a
|
|
102
|
+
fresh report whose `repositoryRevision` matches `graphRevision` and whose observation maps to the
|
|
103
|
+
dynamic file/line resolves that `UNKNOWN`; missing capture remains unknown.
|
|
104
104
|
- **Review clone families or same-name divergence**: `find_duplicates`; framework router boilerplate
|
|
105
|
-
|
|
105
|
+
and immutable declarative catalog shapes stay suppressed unless explicitly included.
|
|
106
106
|
- **Map measured coverage or honest static test reachability**: `coverage_map`; unavailable measured
|
|
107
107
|
coverage is not 0%.
|
|
108
108
|
- **Prioritize local performance-review candidates**: `hot_path_review`; confirm with a profiler or
|
|
109
109
|
benchmark before changing runtime behavior.
|
|
110
110
|
|
|
111
|
-
### Intended architecture
|
|
111
|
+
### Intended architecture
|
|
112
112
|
|
|
113
113
|
- **Read or establish intended architecture**: `get_architecture_contract`. A returned starter adapts
|
|
114
114
|
to Maven/Gradle source roots and monorepos and proposes product-code territories plus observed
|
|
@@ -123,11 +123,8 @@ projection; expand only when the answer requires it.
|
|
|
123
123
|
`verify_architecture`.
|
|
124
124
|
- **Understand or request an exception**: `explain_architecture_violation` ->
|
|
125
125
|
`propose_architecture_exception`; proposals never mutate policy automatically. After human approval,
|
|
126
|
-
the owner must add the returned proposal to the local contract's `exceptions
|
|
127
|
-
|
|
128
|
-
Hosted profile.
|
|
129
|
-
- **Review exactly what Hosted sync would send**: `preview_sync`; only an approved
|
|
130
|
-
`sync_graph dry_run=false confirm_token=...` may send that exact bounded payload.
|
|
126
|
+
the owner must add the returned proposal to the local contract's `exceptions`; remote governance
|
|
127
|
+
belongs to `weavatrix-online`.
|
|
131
128
|
|
|
132
129
|
Across every scenario, treat `PARTIAL`, `UNAVAILABLE`, `OFF`, `NOT_SUPPORTED`, `NOT_CHECKED`,
|
|
133
130
|
`ERROR`, or capped evidence as incomplete rather than success. Java and Rust exact language-server
|
|
@@ -196,22 +193,14 @@ interface dispatch, reflection, or runtime behavior.
|
|
|
196
193
|
ecosystem is not a repository-wide clean zero. For OSV, `OK` is
|
|
197
194
|
complete for the recorded dependency fingerprint; `PARTIAL` is incomplete or stale,
|
|
198
195
|
`NOT_CHECKED` has no repository-specific result, and `ERROR` means the local check failed. Treat
|
|
199
|
-
the same non-`OK` states as incomplete for malware scanning.
|
|
200
|
-
|
|
201
|
-
invoking `refresh_advisories`; enabling the group alone sends nothing.
|
|
196
|
+
the same non-`OK` states as incomplete for malware scanning. Remote OSV refresh belongs to the
|
|
197
|
+
separate `weavatrix-online` package; this MIT core reads only its validated local cache.
|
|
202
198
|
- **Offline by design**: scans and graph queries use local files; the semantic overlay may launch
|
|
203
199
|
Weavatrix's bundled read-only TypeScript language-server child process, but it never runs repository
|
|
204
200
|
scripts or downloads a provider. Coverage tools read existing reports and never run tests. The
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
package names + versions so `run_audit` has fresh vulnerability data) and `sync_graph` (pushes only
|
|
209
|
-
the exact allowlisted graph/evidence contract previously serialized by local-only `preview_sync` to a
|
|
210
|
-
user-configured endpoint; analyzers may read local source, but the wire contract has no body,
|
|
211
|
-
snippet, absolute-host-path or environment fields, and unknown fields are discarded; disabled until
|
|
212
|
-
`WEAVATRIX_SYNC_URL` is set). `pull_architecture_contract` sends only the active repository's opaque
|
|
213
|
-
stable UUID, downloads the owner-approved contract, validates it, and caches it locally; it requires
|
|
214
|
-
`WEAVATRIX_SYNC_URL` and `WEAVATRIX_SYNC_TOKEN`. All three are absent from the default profile.
|
|
201
|
+
package contains no network-touching tool or network-enabled profile. Online/Cloud/Enterprise work
|
|
202
|
+
uses the separate `weavatrix-online` superset, which depends on this core and may add tools, skills
|
|
203
|
+
and local analyzer providers without replacing core implementations.
|
|
215
204
|
- **Repository boundary**: source reads and graph-derived paths are realpath-contained. `open_repo`
|
|
216
205
|
intentionally changes the active boundary through an explicit offline tool call. It is included in
|
|
217
206
|
`offline`, absent from `pinned`, and available in a custom registration only when `retarget` is
|
|
@@ -362,34 +351,20 @@ dependency findings when every use is inside those roots; graph edges, cycles an
|
|
|
362
351
|
visible. `managedDependencies` documents modules provided outside the repo's Python manifest;
|
|
363
352
|
`ignoreDependencies` suppresses intentionally unresolved imports.
|
|
364
353
|
|
|
365
|
-
##
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
package-dependency and clone-review evidence. Read each section's `state`, `verdict` and completeness counts; `PARTIAL`,
|
|
372
|
-
`NOT_CHECKED` and `ERROR` are unknown/incomplete, never a clean result. Architecture evidence
|
|
373
|
-
contains concrete runtime versus compile-time cycles, declared boundary violations and separated
|
|
374
|
-
runtime/type-only/compile-only module dependencies. Package evidence contains a bounded lockfile
|
|
375
|
-
graph with direct/transitive runtime, dev, optional and peer edges plus explicit resolution counts.
|
|
376
|
-
Duplicate evidence contains stable, source-free clone/divergence candidates; it never sends method
|
|
377
|
-
bodies or snippets. Use `payload_version: 2` only when the user explicitly wants graph-only
|
|
378
|
-
compatibility—Weavatrix never silently downgrades. A graph predating current provenance metadata, or one stale against
|
|
379
|
-
the working tree, must be rebuilt first. Sync remains unavailable until the user selects `hosted`
|
|
380
|
-
(or the exact `hosted` capability) and configures `WEAVATRIX_SYNC_URL`.
|
|
354
|
+
## Online extensions
|
|
355
|
+
|
|
356
|
+
This package is the MIT, network-free core. If `refresh_advisories`, `online_status`, `preview_sync`,
|
|
357
|
+
`pull_architecture_contract` or `sync_graph` is required, use the separately installed
|
|
358
|
+
`weavatrix-online` MCP and follow its skill. Do not attempt to enable a legacy profile here: the
|
|
359
|
+
`online`, `osv`, `hosted` and `full` names fail loudly and cannot add network code to this artifact.
|
|
381
360
|
|
|
382
361
|
## Troubleshooting
|
|
383
362
|
|
|
384
363
|
- `Graph unavailable` → `rebuild_graph`; normal graph/health calls automatically refresh an existing
|
|
385
364
|
graph and report `none`, `incremental`, or `full`. `open_repo` can select another valid repository path unless
|
|
386
365
|
the registration deliberately omitted `retarget`.
|
|
387
|
-
-
|
|
388
|
-
|
|
389
|
-
access merely to turn `NOT_CHECKED` into a cosmetic green state.
|
|
390
|
-
- `preview_sync` / `pull_architecture_contract` / `sync_graph` is unavailable → use the `hosted` profile only after
|
|
391
|
-
the user chooses hosted integration; configure `WEAVATRIX_SYNC_URL` and a bearer token for contract
|
|
392
|
-
pull. Profile selection alone never performs a request.
|
|
366
|
+
- An Online-only tool is unavailable → keep this core registration network-free and configure the
|
|
367
|
+
separate `weavatrix-online` MCP only when the user actually wants Cloud or Enterprise integration.
|
|
393
368
|
- `No coverage report` → run the repo's own tests with coverage (`vitest run --coverage`,
|
|
394
369
|
`jest --coverage`, `pytest --cov --cov-report=json`, `go test -coverprofile=coverage.out`),
|
|
395
370
|
then re-call.
|
|
@@ -21,12 +21,12 @@ export function loadArchitectureContract(repoRoot, graphPath) {
|
|
|
21
21
|
}
|
|
22
22
|
const cached = graphPath ? join(dirname(graphPath), 'architecture.contract.json') : null
|
|
23
23
|
return cached && existsSync(cached)
|
|
24
|
-
? readContract(cached, '
|
|
24
|
+
? readContract(cached, 'extension-cache')
|
|
25
25
|
: {contract: null, source: null, error: null}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function writeCachedArchitectureContract(graphPath, input) {
|
|
29
|
-
if (!graphPath) throw new Error('graph path is required for
|
|
29
|
+
if (!graphPath) throw new Error('graph path is required for extension contract cache')
|
|
30
30
|
const contract = normalizeArchitectureContract(input)
|
|
31
31
|
const path = join(dirname(graphPath), 'architecture.contract.json')
|
|
32
32
|
mkdirSync(dirname(path), {recursive: true})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {sortFindings, summarizeFindings} from './findings.js'
|
|
2
|
+
|
|
3
|
+
const VALID_STATUS = new Set(['CHECKED', 'NOT_CHECKED', 'PARTIAL', 'ERROR', 'NOT_SUPPORTED'])
|
|
4
|
+
const VALID_COMPLETENESS = new Set(['COMPLETE', 'PARTIAL'])
|
|
5
|
+
|
|
6
|
+
const providerFailure = (provider, reason) => ({
|
|
7
|
+
id: provider.id,
|
|
8
|
+
extension: provider.extension || null,
|
|
9
|
+
status: 'ERROR',
|
|
10
|
+
completeness: 'PARTIAL',
|
|
11
|
+
detail: reason,
|
|
12
|
+
findings: [],
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const normalizeResult = (provider, value) => {
|
|
16
|
+
if (!value || typeof value !== 'object') return providerFailure(provider, 'provider returned no result envelope')
|
|
17
|
+
const status = VALID_STATUS.has(value.status) ? value.status : 'ERROR'
|
|
18
|
+
const completeness = VALID_COMPLETENESS.has(value.completeness) ? value.completeness : 'PARTIAL'
|
|
19
|
+
const findings = Array.isArray(value.findings) ? value.findings.map((finding) => ({
|
|
20
|
+
...finding,
|
|
21
|
+
source: finding?.source || `extension:${provider.extension || provider.id}`,
|
|
22
|
+
})) : []
|
|
23
|
+
return {
|
|
24
|
+
id: provider.id,
|
|
25
|
+
extension: provider.extension || null,
|
|
26
|
+
status,
|
|
27
|
+
completeness,
|
|
28
|
+
detail: String(value.detail || (status === 'ERROR' ? 'provider returned an invalid status' : 'extension analyzer completed')),
|
|
29
|
+
findings,
|
|
30
|
+
evidence: value.evidence ?? null,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Extension analyzers are deliberately local. A provider failure stays explicit ERROR/PARTIAL and
|
|
35
|
+
// never converts missing evidence into a clean result for the core audit.
|
|
36
|
+
export async function applyAuditExtensions(coreAudit, {providers = [], repoRoot, graph, args = {}} = {}) {
|
|
37
|
+
if (!coreAudit?.ok || !providers.length) return coreAudit
|
|
38
|
+
const results = []
|
|
39
|
+
for (const provider of providers) {
|
|
40
|
+
try {
|
|
41
|
+
const value = await provider.run({repoRoot, graph, args, coreAudit})
|
|
42
|
+
results.push(normalizeResult(provider, value))
|
|
43
|
+
} catch (error) {
|
|
44
|
+
results.push(providerFailure(provider, error instanceof Error ? error.message : String(error)))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const findings = sortFindings([
|
|
48
|
+
...coreAudit.findings,
|
|
49
|
+
...results.flatMap((result) => result.findings),
|
|
50
|
+
])
|
|
51
|
+
return {
|
|
52
|
+
...coreAudit,
|
|
53
|
+
findings,
|
|
54
|
+
summary: summarizeFindings(findings),
|
|
55
|
+
extensionCapabilities: results.map(({findings: providerFindings, ...result}) => ({
|
|
56
|
+
...result,
|
|
57
|
+
findingCount: providerFindings.length,
|
|
58
|
+
})),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -44,6 +44,11 @@ export function isFrameworkBoilerplateCloneGroup(group) {
|
|
|
44
44
|
&& /^(?:router|routes)\(?\)?$/i.test(String(member.label || '').trim()))
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export function isDeclarativeCatalogCloneGroup(group) {
|
|
48
|
+
const members = Array.isArray(group?.members) ? group.members : []
|
|
49
|
+
return members.length >= 2 && members.every((member) => member.declarative === true)
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
export function analyzeDuplicateGroups(repoRoot, graphPath, args = {}) {
|
|
48
53
|
const settings = {
|
|
49
54
|
simMin: Math.min(100, Math.max(50, Number(args.min_similarity) || 80)),
|
|
@@ -52,12 +57,18 @@ export function analyzeDuplicateGroups(repoRoot, graphPath, args = {}) {
|
|
|
52
57
|
skipTests: args.include_tests !== true,
|
|
53
58
|
includeClassified: args.include_classified === true || args.include_non_product === true,
|
|
54
59
|
includeBoilerplate: args.include_boilerplate === true,
|
|
60
|
+
includeDeclarative: args.include_declarative === true,
|
|
55
61
|
}
|
|
56
62
|
const data = computeDuplicates(repoRoot, graphPath, {includeStrings: args.include_strings === true, minTokens: settings.tokMin})
|
|
57
63
|
const allGroups = groupPairs(data, settings)
|
|
58
|
-
const
|
|
64
|
+
const afterBoilerplate = settings.includeBoilerplate ? allGroups : allGroups.filter((group) => !isFrameworkBoilerplateCloneGroup(group))
|
|
65
|
+
const groups = settings.includeDeclarative ? afterBoilerplate : afterBoilerplate.filter((group) => !isDeclarativeCatalogCloneGroup(group))
|
|
59
66
|
const suppressed = (data.frags || []).filter((fragment) => !eligible(fragment, settings)).length
|
|
60
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
settings, groups, suppressed,
|
|
69
|
+
boilerplateSuppressed: allGroups.length - afterBoilerplate.length,
|
|
70
|
+
declarativeSuppressed: afterBoilerplate.length - groups.length,
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
const digest = (value) => createHash('sha256').update(value).digest('hex').slice(0, 20)
|
|
@@ -111,7 +111,7 @@ function symbolRanges(graph) {
|
|
|
111
111
|
if (!line) continue;
|
|
112
112
|
let arr = byFile.get(n.source_file);
|
|
113
113
|
if (!arr) byFile.set(n.source_file, (arr = []));
|
|
114
|
-
arr.push({ id: n.id, label: n.label || n.id, line });
|
|
114
|
+
arr.push({ id: n.id, label: n.label || n.id, line, symbolKind: n.symbol_kind || "" });
|
|
115
115
|
}
|
|
116
116
|
for (const arr of byFile.values()) arr.sort((a, b) => a.line - b.line);
|
|
117
117
|
return byFile;
|
|
@@ -175,9 +175,14 @@ export function computeDuplicates(repoPath, graphJsonPath, opts = {}) {
|
|
|
175
175
|
if (end - start < 2) continue;
|
|
176
176
|
const toks = tokenize(stripNonCode(body.join("\n"), py));
|
|
177
177
|
if (toks.strict.length < scanTokenFloor) continue;
|
|
178
|
+
const strippedBody = stripNonCode(body.join("\n"), py);
|
|
179
|
+
const declarativeValue = /^(?:export\s+)?(?:const|let|var)\s+[\w$]+(?:\s*:[^=]+)?\s*=\s*(?:Object\.(?:freeze|seal)\s*\(\s*)?[\[{]/s.test(strippedBody.trim())
|
|
180
|
+
&& !/=>|\b(?:function|if|for|while|switch|try|await|yield|return|throw|new)\b/.test(strippedBody);
|
|
181
|
+
const declarativeFactory = /^(?:export\s+)?const\s+[\w$]+(?:\s*:[^=]+)?\s*=\s*(?:sqliteTable|pgTable|mysqlTable)\s*\(/s.test(strippedBody.trim());
|
|
182
|
+
const declarative = declarativeValue || declarativeFactory || /^(?:interface|type|enum)$/i.test(syms[i].symbolKind);
|
|
178
183
|
frags.push({
|
|
179
184
|
id: syms[i].id, label: syms[i].label, file, start, end,
|
|
180
|
-
n: toks.strict.length,
|
|
185
|
+
n: toks.strict.length, declarative,
|
|
181
186
|
...classificationFields(pathInfo),
|
|
182
187
|
fp: { strict: fingerprints(toks.strict), renamed: fingerprints(toks.renamed) },
|
|
183
188
|
});
|
|
@@ -39,6 +39,7 @@ export function buildHealthCapabilityMatrix({
|
|
|
39
39
|
const noDependencyEvidence = ecosystemRows.length === 0;
|
|
40
40
|
const onlyUnsupportedDependencies = unsupportedDependencyRows.length > 0 && supportedDependencyRows.length === 0;
|
|
41
41
|
const advisorySupported = supportedDependencyRows.some((item) => ["npm", "go", "python", "rust", "maven", "gradle"].includes(item.ecosystem));
|
|
42
|
+
const onlyRustAdvisories = supportedDependencyRows.length > 0 && supportedDependencyRows.every((item) => item.ecosystem === "rust");
|
|
42
43
|
const malwareSupported = supportedDependencyRows.some((item) => ["npm", "go", "python"].includes(item.ecosystem));
|
|
43
44
|
const coverageSupported = [...languages].some((language) => ["javascript/typescript", "python", "go"].includes(language));
|
|
44
45
|
const runtimeFiles = Number(correctnessCoverage.runtimeCorrectnessFiles || 0);
|
|
@@ -71,7 +72,7 @@ export function buildHealthCapabilityMatrix({
|
|
|
71
72
|
concurrency: concurrencyFiles
|
|
72
73
|
? capability("CHECKED", "PARTIAL", `Checked ${concurrencyFiles} Java source file(s) for direct InterruptedException restore/rethrow evidence. No race detector ran; race freedom is not claimed.`)
|
|
73
74
|
: capability("NOT_SUPPORTED", "PARTIAL", "No supported Java interruption check applied. No race detector ran; race freedom is not claimed."),
|
|
74
|
-
advisories: checkCapability(checks.osv, advisorySupported, "OSV advisory matching"),
|
|
75
|
+
advisories: checkCapability(onlyRustAdvisories && checks.rustsec?.status === "OK" ? checks.rustsec : checks.osv, advisorySupported, onlyRustAdvisories ? "RustSec advisory matching" : "OSV advisory matching"),
|
|
75
76
|
malware: checkCapability(checks.malware, malwareSupported, "Installed-package malware scanning"),
|
|
76
77
|
coverage: measuredCoverageFiles > 0
|
|
77
78
|
? capability("CHECKED", "COMPLETE", `Mapped an existing supported coverage report to ${measuredCoverageFiles} file(s).`)
|
|
@@ -3,6 +3,7 @@ import { advisoryQueryFingerprint, loadStore, queryStore } from "../../security/
|
|
|
3
3
|
import { matchAdvisories } from "../../security/match.js";
|
|
4
4
|
import { scanMalware } from "../../security/malware-heuristics.js";
|
|
5
5
|
import { classifyTyposquat } from "../../security/typosquat.js";
|
|
6
|
+
import { loadRustAdvisoryReport } from "../../security/rust-advisory-report.js";
|
|
6
7
|
import { makeFinding } from "../findings.js";
|
|
7
8
|
import { packageReachability } from "../package-reachability.js";
|
|
8
9
|
|
|
@@ -31,8 +32,9 @@ export async function runSupplyChainChecks(repoPath, {
|
|
|
31
32
|
let installedCount = 0;
|
|
32
33
|
let inst = { installed: [], drift: [] };
|
|
33
34
|
const checks = {
|
|
34
|
-
osv: { status: "NOT_CHECKED", detail: "Advisory cache was never refreshed for this repository.
|
|
35
|
+
osv: { status: "NOT_CHECKED", detail: "Advisory cache was never refreshed for this repository. The MIT core remains offline; use the separate Weavatrix Online connector only if you choose to query OSV with pinned package names and versions." },
|
|
35
36
|
malware: { status: skipMalwareScan ? "NOT_CHECKED" : "PENDING", detail: skipMalwareScan ? "Installed-package malware scan is opt-in and was not requested." : "" },
|
|
37
|
+
rustsec: { status: "NOT_CHECKED", detail: "No saved cargo audit --json report was inspected." },
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
try {
|
|
@@ -48,7 +50,7 @@ export async function runSupplyChainChecks(repoPath, {
|
|
|
48
50
|
const coverage = typeof repoStamp === "object" && Number.isFinite(repoStamp.queried)
|
|
49
51
|
? ` (${repoStamp.queried_ok ?? repoStamp.queried}/${repoStamp.queried} package versions queried successfully)`
|
|
50
52
|
: "";
|
|
51
|
-
const drift = fingerprintMatches ? "" : " Dependency versions changed, or this is a legacy stamp without a package fingerprint;
|
|
53
|
+
const drift = fingerprintMatches ? "" : " Dependency versions changed, or this is a legacy stamp without a package fingerprint; refresh through the separate Weavatrix Online connector for complete advisory coverage.";
|
|
52
54
|
checks.osv = {
|
|
53
55
|
status,
|
|
54
56
|
detail: `${status === "PARTIAL" ? "Partially matched" : "Matched"} installed packages against the cached OSV snapshot from ${advisoryDbDate}${coverage}.${drift}`,
|
|
@@ -114,6 +116,19 @@ export async function runSupplyChainChecks(repoPath, {
|
|
|
114
116
|
fixHint: "npm ci (clean install from the lockfile)",
|
|
115
117
|
}));
|
|
116
118
|
}
|
|
119
|
+
const rustsec = loadRustAdvisoryReport(repoPath);
|
|
120
|
+
checks.rustsec = { status: rustsec.status, detail: rustsec.detail, checkedAt: rustsec.checkedAt || null };
|
|
121
|
+
for (const issue of rustsec.findings) {
|
|
122
|
+
findings.push(makeFinding({
|
|
123
|
+
category: "vulnerability", rule: issue.kind === "vulnerability" ? "known-vuln" : "rust-advisory-warning",
|
|
124
|
+
severity: issue.kind === "vulnerability" ? "high" : "medium", confidence: "high",
|
|
125
|
+
title: `${issue.id}: ${issue.package}${issue.version ? `@${issue.version}` : ""}`,
|
|
126
|
+
detail: `${issue.title}${issue.patched.length ? ` Patched: ${issue.patched.join(", ")}.` : ""}`,
|
|
127
|
+
package: issue.package, version: issue.version, source: "rustsec",
|
|
128
|
+
evidence: issue.url ? [{ file: issue.url, line: 0, snippet: `imported from ${rustsec.file}` }] : [{ file: rustsec.file, line: 0, snippet: issue.id }],
|
|
129
|
+
fixHint: issue.patched.length ? `upgrade ${issue.package} to a patched RustSec range (${issue.patched.join(", ")})` : `review or replace ${issue.package}; no patched range was reported`,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
117
132
|
} catch (error) {
|
|
118
133
|
checks.osv = { status: "ERROR", detail: `Offline advisory matching failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
119
134
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const concreteVersion = (value) => !!value && !/[\[\](),${}*+]/.test(String(value));
|
|
6
|
+
const safeEntries = (path) => { try { return readdirSync(path, { withFileTypes: true }); } catch { return []; } };
|
|
7
|
+
|
|
8
|
+
function findEndOfCentralDirectory(buffer) {
|
|
9
|
+
const minimum = Math.max(0, buffer.length - 65_557);
|
|
10
|
+
for (let offset = buffer.length - 22; offset >= minimum; offset--) {
|
|
11
|
+
if (buffer.readUInt32LE(offset) === 0x06054b50) return offset;
|
|
12
|
+
}
|
|
13
|
+
return -1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// JARs are ZIP files. Reading only the central directory avoids extraction and code execution.
|
|
17
|
+
export function readJarClassNames(path, { maxClasses = 250_000 } = {}) {
|
|
18
|
+
const buffer = readFileSync(path);
|
|
19
|
+
const end = findEndOfCentralDirectory(buffer);
|
|
20
|
+
if (end < 0) throw new Error("ZIP central directory is missing");
|
|
21
|
+
const entries = buffer.readUInt16LE(end + 10);
|
|
22
|
+
let offset = buffer.readUInt32LE(end + 16);
|
|
23
|
+
const classes = new Set(), packages = new Set();
|
|
24
|
+
let truncated = false;
|
|
25
|
+
for (let index = 0; index < entries && offset + 46 <= buffer.length; index++) {
|
|
26
|
+
if (buffer.readUInt32LE(offset) !== 0x02014b50) throw new Error("invalid ZIP central directory entry");
|
|
27
|
+
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
28
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
29
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
30
|
+
let name = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8");
|
|
31
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
32
|
+
name = name.replace(/^META-INF\/versions\/\d+\//, "");
|
|
33
|
+
if (!name.endsWith(".class") || /(?:^|\/)(?:module-info|package-info)\.class$/.test(name)) continue;
|
|
34
|
+
const identity = name.slice(0, -6).replace(/\$/g, ".").replace(/\//g, ".");
|
|
35
|
+
classes.add(identity);
|
|
36
|
+
packages.add(identity.includes(".") ? identity.slice(0, identity.lastIndexOf(".")) : "");
|
|
37
|
+
if (classes.size >= maxClasses) { truncated = true; break; }
|
|
38
|
+
}
|
|
39
|
+
return { classes, packages, truncated };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function artifactCandidates(dependency, home) {
|
|
43
|
+
if (!dependency.group || !dependency.artifact || !concreteVersion(dependency.version)) return [];
|
|
44
|
+
const file = `${dependency.artifact}-${dependency.version}.jar`;
|
|
45
|
+
const maven = join(home, ".m2", "repository", ...dependency.group.split("."), dependency.artifact, dependency.version, file);
|
|
46
|
+
const gradleRoot = join(home, ".gradle", "caches", "modules-2", "files-2.1", dependency.group, dependency.artifact, dependency.version);
|
|
47
|
+
const gradle = safeEntries(gradleRoot).filter((entry) => entry.isDirectory()).flatMap((entry) =>
|
|
48
|
+
safeEntries(join(gradleRoot, entry.name)).filter((child) => child.isFile() && child.name.endsWith(".jar"))
|
|
49
|
+
.map((child) => join(gradleRoot, entry.name, child.name)));
|
|
50
|
+
return [maven, ...gradle].filter((candidate, index, all) => existsSync(candidate) && all.indexOf(candidate) === index);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const addOwner = (map, key, owner) => {
|
|
54
|
+
const owners = map.get(key) || new Set();
|
|
55
|
+
owners.add(owner); map.set(key, owners);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Exact import -> artifact evidence from already-installed Maven/Gradle JARs. This is offline and
|
|
59
|
+
// read-only: no build tool, plugin, class loader or repository code is executed.
|
|
60
|
+
export function collectJvmArtifactIndex(dependencies, { home = homedir(), maxArtifacts = 256, maxClasses = 500_000 } = {}) {
|
|
61
|
+
const unique = [...new Map(dependencies.map((dependency) => [dependency.name, dependency])).values()];
|
|
62
|
+
const classes = new Map(), packages = new Map(), errors = [];
|
|
63
|
+
let artifactsIndexed = 0, artifactsMissing = 0, classCount = 0, truncated = false;
|
|
64
|
+
for (const dependency of unique) {
|
|
65
|
+
if (artifactsIndexed >= maxArtifacts || classCount >= maxClasses) { truncated = true; break; }
|
|
66
|
+
const candidates = artifactCandidates(dependency, home);
|
|
67
|
+
if (!candidates.length) { artifactsMissing++; continue; }
|
|
68
|
+
try {
|
|
69
|
+
const remaining = Math.max(1, maxClasses - classCount);
|
|
70
|
+
const content = readJarClassNames(candidates[0], { maxClasses: remaining });
|
|
71
|
+
for (const identity of content.classes) addOwner(classes, identity, dependency.name);
|
|
72
|
+
for (const identity of content.packages) addOwner(packages, identity, dependency.name);
|
|
73
|
+
classCount += content.classes.size; artifactsIndexed++; truncated ||= content.truncated;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
errors.push(`${dependency.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const resolve = (spec) => {
|
|
79
|
+
const imported = String(spec || "").replace(/\.\*$/, "");
|
|
80
|
+
const owners = String(spec || "").endsWith(".*") ? packages.get(imported) : classes.get(imported);
|
|
81
|
+
return [...(owners || [])].sort();
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
resolve, artifactsRequired: unique.length, artifactsIndexed, artifactsMissing,
|
|
85
|
+
classCount, errors, truncated,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRepoBoundary } from "../repo-path.js";
|
|
2
2
|
import { dependencyVerification, makeFinding } from "./findings.js";
|
|
3
3
|
import { parseGradleDependencies, parseGradleVersionCatalog, parseMavenPom } from "./jvm-manifests.js";
|
|
4
|
+
import { collectJvmArtifactIndex } from "./jvm-artifact-index.js";
|
|
4
5
|
import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
|
|
5
6
|
|
|
6
7
|
const normalize = (value) => String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
@@ -24,12 +25,18 @@ function bestDependency(spec, dependencies) {
|
|
|
24
25
|
return ranked[0].dependency;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function analyze(ecosystem, manifests, dependencies, imports, unresolvedDeclarations, mappingDependencies = dependencies, includeMissing = true) {
|
|
28
|
+
function analyze(ecosystem, manifests, dependencies, imports, unresolvedDeclarations, mappingDependencies = dependencies, includeMissing = true, artifactEvidence) {
|
|
28
29
|
const findings = [], used = new Map(), missing = new Map();
|
|
29
30
|
const owned = new Set(dependencies.map((dependency) => dependency.name));
|
|
31
|
+
let exactMappedImports = 0, heuristicMappedImports = 0, ambiguousImports = 0;
|
|
30
32
|
for (const item of imports) {
|
|
31
|
-
const
|
|
33
|
+
const exactOwners = artifactEvidence.resolve(item.spec || item.pkg);
|
|
34
|
+
if (exactOwners.length > 1) ambiguousImports++;
|
|
35
|
+
const dependency = exactOwners.length === 1
|
|
36
|
+
? mappingDependencies.find((candidate) => candidate.name === exactOwners[0])
|
|
37
|
+
: bestDependency(item.spec || item.pkg, mappingDependencies);
|
|
32
38
|
if (dependency) {
|
|
39
|
+
if (exactOwners.length === 1) exactMappedImports++; else heuristicMappedImports++;
|
|
33
40
|
if (owned.has(dependency.name)) {
|
|
34
41
|
const list = used.get(dependency.name) || [];
|
|
35
42
|
list.push(item); used.set(dependency.name, list);
|
|
@@ -69,21 +76,36 @@ function analyze(ecosystem, manifests, dependencies, imports, unresolvedDeclarat
|
|
|
69
76
|
}));
|
|
70
77
|
}
|
|
71
78
|
const present = manifests.length > 0;
|
|
79
|
+
const exactComplete = present && unresolvedDeclarations === 0 && !artifactEvidence.truncated
|
|
80
|
+
&& artifactEvidence.errors.length === 0 && artifactEvidence.artifactsMissing === 0
|
|
81
|
+
&& artifactEvidence.artifactsIndexed === artifactEvidence.artifactsRequired
|
|
82
|
+
&& exactMappedImports === imports.length && ambiguousImports === 0;
|
|
72
83
|
return {
|
|
73
84
|
present,
|
|
74
85
|
status: present ? "CHECKED" : "NOT_PRESENT",
|
|
75
|
-
completeness: present ? (
|
|
86
|
+
completeness: present ? (exactComplete ? "COMPLETE" : "PARTIAL") : "NOT_APPLICABLE",
|
|
76
87
|
manifests,
|
|
77
88
|
declared: dependencies.length,
|
|
78
89
|
mappedImports: [...used.values()].reduce((sum, list) => sum + list.length, 0),
|
|
79
90
|
unmappedImports: [...missing.values()].reduce((sum, list) => sum + list.length, 0),
|
|
80
91
|
unresolvedDeclarations,
|
|
92
|
+
exactArtifactEvidence: {
|
|
93
|
+
artifactsRequired: artifactEvidence.artifactsRequired,
|
|
94
|
+
artifactsIndexed: artifactEvidence.artifactsIndexed,
|
|
95
|
+
artifactsMissing: artifactEvidence.artifactsMissing,
|
|
96
|
+
classesIndexed: artifactEvidence.classCount,
|
|
97
|
+
exactMappedImports,
|
|
98
|
+
heuristicMappedImports,
|
|
99
|
+
ambiguousImports,
|
|
100
|
+
truncated: artifactEvidence.truncated,
|
|
101
|
+
errors: artifactEvidence.errors.slice(0, 10),
|
|
102
|
+
},
|
|
81
103
|
sample: dependencies.slice(0, 20).map(({ file, name, version }) => ({ file, identity: name, version })),
|
|
82
104
|
reason: !present
|
|
83
105
|
? `No ${ecosystem === "maven" ? "pom.xml" : "Gradle build file"} was discovered.`
|
|
84
|
-
:
|
|
85
|
-
? `${dependencies.length} declarations
|
|
86
|
-
: `${dependencies.length} declarations
|
|
106
|
+
: exactComplete
|
|
107
|
+
? `${dependencies.length} declarations were compared with every indexed non-JDK Java import using exact class ownership from ${artifactEvidence.artifactsIndexed} installed JAR(s).`
|
|
108
|
+
: `${dependencies.length} declarations and every indexed non-JDK Java import were checked, but exact artifact evidence is partial: ${unresolvedDeclarations} unresolved declaration(s), ${artifactEvidence.artifactsMissing} installed JAR(s) missing, ${heuristicMappedImports} heuristic mapping(s), ${ambiguousImports} ambiguous mapping(s). Heuristic missing/unused findings remain review evidence, never compiler proof.`,
|
|
87
109
|
findings,
|
|
88
110
|
};
|
|
89
111
|
}
|
|
@@ -108,8 +130,9 @@ export function collectJvmDependencyEvidence(repoRoot, { files = listRepoFiles(r
|
|
|
108
130
|
});
|
|
109
131
|
const javaImports = externalImports.filter((entry) => entry.ecosystem === "Maven" && entry.pkg && !entry.builtin && !entry.unresolved);
|
|
110
132
|
const allDependencies = [...mavenDependencies, ...gradleDependencies];
|
|
133
|
+
const artifactEvidence = collectJvmArtifactIndex(allDependencies);
|
|
111
134
|
return {
|
|
112
|
-
maven: analyze("maven", mavenFiles, mavenDependencies, javaImports, mavenUnresolved, allDependencies, true),
|
|
113
|
-
gradle: analyze("gradle", [...gradleFiles, ...catalogFiles], gradleDependencies, javaImports, gradleUnresolved, allDependencies, mavenFiles.length === 0),
|
|
135
|
+
maven: analyze("maven", mavenFiles, mavenDependencies, javaImports, mavenUnresolved, allDependencies, true, artifactEvidence),
|
|
136
|
+
gradle: analyze("gradle", [...gradleFiles, ...catalogFiles], gradleDependencies, javaImports, gradleUnresolved, allDependencies, mavenFiles.length === 0, artifactEvidence),
|
|
114
137
|
};
|
|
115
138
|
}
|