Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ the compatibility and migration notes before upgrading.
(`GET /v1/supply-chain/exports/{id}/derived.spdx.json`) names GraphNest as
creator, links the preserved original, and carries assessments as comments
only. Migration 035 adds imports and upload grants.
- Review workflows: a queue of occurrences needing review, human license
conclusions recorded as immutable evidence, scoped approve/reject/exception
decisions with optimistic concurrency on the evidence fingerprint
(`409 stale_basis`), versioned policies evaluated over the SPDX expression
tree with a clearly labelled example fixture and no auto-approval of
unknowns, repository-scoped review grants, and an append-only audit trail
under `/v1/supply-chain/review/*` and `/v1/supply-chain/policies`.
Migration 036 adds the review tables.
- Read-only MCP tools `search_dependency_inventory`,
`find_component_repositories`, and `inspect_component_license` over the
same authorized services as REST.
- Retention for inventory snapshots (`GRAPHNEST_SUPPLY_CHAIN_RETAIN_SNAPSHOTS`)
that always preserves the current snapshot and any snapshot referenced by
a review record, plus bounded collection and job history.

## [0.5.0] - 2026-09-18

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ What the inventory is and is not:
- A failed refresh (403, 404, rate limit, malformed or oversized document, outage) records a collection attempt and leaves the last successful snapshot in place; the status reports `collection: failed` alongside the retained inventory.
- Inventory eligibility is repository authorization alone. It works for repositories with no Zoekt index, no SCIP upload, and no graph enrichment, and inventory work never blocks lexical indexing.

License review is a separate, auditable layer: reviewers with a repository-scoped grant record human conclusions and approve/reject/exception decisions against the exact evidence they saw (a changed evidence fingerprint is refused), versioned policies are evaluated over the SPDX expression tree (the shipped policy is a labelled example, and unknown licensing never auto-approves), and three read-only MCP tools expose the inventory to agents through the same authorization as REST.

SBOMs produced elsewhere (Syft, ORT, or any tool writing SPDX 2.3 JSON or CycloneDX 1.6 JSON) can be imported into separate `import:<subject>:<label>` streams with `POST /v1/supply-chain/imports`; the uploader is recorded apart from the producer the document claims, and a derived SPDX export links back to the preserved original. Portfolio views (`/v1/supply-chain/overview`, `/components`, `/facets`, exports, comparison) aggregate only over the caller's authorized repositories and name every denominator.

Every read resolves the live principal's repository scope before any inventory row is touched; snapshot and job identifiers outside that scope are indistinguishable from missing ones. Manual refresh (`POST /v1/supply-chain/repositories/{id}/refresh`) only enqueues a bounded background job and requires administrator access. The published snapshot is also projected into the existing GitHub-sourced SCIP package mappings; manual mappings are never touched. See [Operations](docs/operations.md#dependencies--licenses-inventory) and [ADR-0017](docs/adr/0017-supply-chain-inventory.md).
Expand Down
12 changes: 12 additions & 0 deletions cmd/graphnest-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,18 @@ func startSupplyChain(ctx context.Context, settings config.SupplyChain, store *p
if _, err := scheduler.Tick(ctx); err != nil && ctx.Err() == nil {
logger.Error("supply chain scheduling failed", "error", err)
}
if settings.RetainSnapshots > 0 {
if _, _, err := store.PruneSupplyChainSnapshots(ctx, settings.RetainSnapshots, 200); err != nil && ctx.Err() == nil {
logger.Error("supply chain snapshot retention failed", "error", err)
}
}
if _, _, err := store.PruneSupplyChainHistory(ctx, 50, 30*24*time.Hour); err != nil && ctx.Err() == nil {
logger.Error("supply chain history retention failed", "error", err)
}
if depths, err := store.EnrichmentQueueDepths(ctx); err == nil {
metrics.SetSupplyChainQueueDepth("enrichment_queued", depths["queued"])
metrics.SetSupplyChainQueueDepth("enrichment_running", depths["running"])
}
}
tick()
ticker := time.NewTicker(reconcileInterval)
Expand Down
1 change: 1 addition & 0 deletions deploy/helm/graphnest/ci/optional-values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ server:
workers: 2
maxDocumentBytes: 33554432
maxComponents: 60000
retainSnapshots: 5
registries:
npm: {url: https://npm.example.invalid/, namespaces: ["@acme", "@internal"], allowPrivate: false, token: true}
nuget: {url: "", namespaces: [], allowPrivate: false}
Expand Down
1 change: 1 addition & 0 deletions deploy/helm/graphnest/templates/configmaps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ data:
GRAPHNEST_SUPPLY_CHAIN_WORKERS: {{ .Values.server.supplyChain.workers | quote }}
GRAPHNEST_SUPPLY_CHAIN_MAX_DOCUMENT_BYTES: {{ printf "%d" (int64 .Values.server.supplyChain.maxDocumentBytes) | quote }}
GRAPHNEST_SUPPLY_CHAIN_MAX_COMPONENTS: {{ .Values.server.supplyChain.maxComponents | quote }}
GRAPHNEST_SUPPLY_CHAIN_RETAIN_SNAPSHOTS: {{ .Values.server.supplyChain.retainSnapshots | quote }}
{{- range $ecosystem, $route := (pick .Values.server.supplyChain.registries "npm" "nuget" "maven") }}
{{- if $route.url }}
{{- $prefix := printf "GRAPHNEST_SUPPLY_CHAIN_REGISTRY_%s_" (upper $ecosystem) }}
Expand Down
1 change: 1 addition & 0 deletions deploy/helm/graphnest/tests/render.sh
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ require 'GRAPHNEST_SUPPLY_CHAIN_INTERVAL: "12h"' "$tmp/optional.yaml"
require 'GRAPHNEST_SUPPLY_CHAIN_WORKERS: "2"' "$tmp/optional.yaml"
require 'GRAPHNEST_SUPPLY_CHAIN_MAX_DOCUMENT_BYTES: "33554432"' "$tmp/optional.yaml"
require 'GRAPHNEST_SUPPLY_CHAIN_MAX_COMPONENTS: "60000"' "$tmp/optional.yaml"
require 'GRAPHNEST_SUPPLY_CHAIN_RETAIN_SNAPSHOTS: "5"' "$tmp/optional.yaml"
require 'GRAPHNEST_SUPPLY_CHAIN_REGISTRY_NPM_URL: "https://npm.example.invalid/"' "$tmp/optional.yaml"
require 'GRAPHNEST_SUPPLY_CHAIN_REGISTRY_NPM_NAMESPACES: "@acme,@internal"' "$tmp/optional.yaml"
require 'GRAPHNEST_SUPPLY_CHAIN_REGISTRY_NPM_TOKEN_FILE: /var/run/secrets/graphnest/registries/npm-token' "$tmp/optional.yaml"
Expand Down
3 changes: 2 additions & 1 deletion deploy/helm/graphnest/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,13 +279,14 @@
"supplyChain": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "interval", "workers", "maxDocumentBytes", "maxComponents", "registries"],
"required": ["enabled", "interval", "workers", "maxDocumentBytes", "maxComponents", "retainSnapshots", "registries"],
"properties": {
"enabled": {"type": "boolean"},
"interval": {"$ref": "#/definitions/duration"},
"workers": {"type": "integer", "minimum": 1, "maximum": 8},
"maxDocumentBytes": {"type": "integer", "minimum": 1, "maximum": 268435456},
"maxComponents": {"type": "integer", "minimum": 1, "maximum": 500000},
"retainSnapshots": {"type": "integer", "minimum": 0, "maximum": 1000},
"registries": {
"type": "object",
"additionalProperties": false,
Expand Down
2 changes: 2 additions & 0 deletions deploy/helm/graphnest/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ server:
workers: 1
maxDocumentBytes: 16777216
maxComponents: 50000
# Snapshots kept per repository stream beyond those referenced by reviews; 0 disables pruning.
retainSnapshots: 10
# License enrichment routes; an ecosystem without a url produces no registry traffic.
registries:
npm: {url: "", namespaces: [], allowPrivate: false, token: false}
Expand Down
Loading
Loading