Skip to content

Commit 06f21b8

Browse files
authored
ci: supply-chain hardening — Dependabot, govulncheck, fuzz targets (#70)
Adds three input/dependency-robustness layers without changing the CI gate's pass/fail contract: - Dependabot (.github/dependabot.yml): weekly, grouped updates for gomod (root + sdk/go), pip (tools), npm (web + docs), and github-actions. Grouped to keep PR volume low. Security alerts are independent of this config, so grouping version updates does not delay security fixes. - govulncheck: `make vulncheck` target + a non-blocking CI step. Kept non-blocking for now because most current findings are Go std-lib CVEs that track the toolchain version (cleared by bumping Go), not the repo's own deps. The comment notes to drop continue-on-error once findings are triaged. - Fuzz targets on two untrusted-input surfaces: - FuzzParse (internal/query): the SPL parser must never panic on arbitrary input. Seed corpus runs on every `go test`. - FuzzConfineImportPath (internal/umodel): the import-root confinement must never accept a path that escapes the root. Seeds include traversal and NUL bytes. Both ran 2M+ executions under `-fuzz` with no crash; the seed corpora double as fast regression tests in the normal suite.
1 parent 214435b commit 06f21b8

5 files changed

Lines changed: 142 additions & 1 deletion

File tree

.github/dependabot.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Dependency update automation. Grouped + weekly to keep PR volume low.
2+
version: 2
3+
updates:
4+
- package-ecosystem: gomod
5+
directory: "/"
6+
schedule:
7+
interval: weekly
8+
groups:
9+
go-modules:
10+
patterns: ["*"]
11+
- package-ecosystem: gomod
12+
directory: "/sdk/go"
13+
schedule:
14+
interval: weekly
15+
groups:
16+
go-sdk:
17+
patterns: ["*"]
18+
- package-ecosystem: pip
19+
directory: "/tools"
20+
schedule:
21+
interval: weekly
22+
- package-ecosystem: npm
23+
directory: "/web"
24+
schedule:
25+
interval: weekly
26+
groups:
27+
web:
28+
patterns: ["*"]
29+
- package-ecosystem: npm
30+
directory: "/docs"
31+
schedule:
32+
interval: weekly
33+
groups:
34+
docs:
35+
patterns: ["*"]
36+
- package-ecosystem: github-actions
37+
directory: "/"
38+
schedule:
39+
interval: weekly

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ jobs:
7878
- name: Run CI
7979
run: make ci
8080

81+
# Vulnerability scan. Non-blocking for now: most findings are Go std-lib
82+
# CVEs that track the toolchain version (cleared by bumping Go), not the
83+
# repo's own deps. Drop continue-on-error once findings are triaged.
84+
- name: Vulnerability scan (govulncheck)
85+
run: make vulncheck
86+
continue-on-error: true
87+
8188
- name: Run Playwright UI tests
8289
run: |
8390
make quickstart &

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.PHONY: help check-env install-env setup setup-ui expand doc docs-schema docs-schema-check example-validate check-manifest
2-
.PHONY: build build-service build-cli install-cli build-ui build-sdk-go dev quickstart dev-api dev-web deploy serve-ui status stop-all stop-dev stop-deploy test test-service test-ui test-ui-e2e test-capability test-quickstart-health test-ladybug verify verify-go verify-python verify-java guard ci clean
2+
.PHONY: build build-service build-cli install-cli build-ui build-sdk-go dev quickstart dev-api dev-web deploy serve-ui status stop-all stop-dev stop-deploy test test-service test-ui test-ui-e2e test-capability test-quickstart-health test-ladybug vulncheck verify verify-go verify-python verify-java guard ci clean
33

44
VENV_PYTHON := .venv/bin/python
55
CONDA_PYTHON := $(if $(CONDA_PREFIX),$(CONDA_PREFIX)/bin/python)
@@ -135,6 +135,11 @@ serve-ui: build-ui
135135
test-service:
136136
go test -race ./...
137137

138+
# Scan for known vulnerabilities (the repo's modules + reachable stdlib). Std-lib
139+
# findings track the Go toolchain version; keep it current to clear them.
140+
vulncheck:
141+
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
142+
138143
test-ui:
139144
@PNPM="$(PNPM)" bash ./scripts/env.sh web-build
140145

internal/query/fuzz_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package query
2+
3+
import (
4+
"testing"
5+
6+
"github.com/alibaba/UnifiedModel/pkg/model"
7+
)
8+
9+
// FuzzParse hardens the SPL parser, an untrusted-input surface. The invariant is
10+
// simple but important: Parse must never panic on arbitrary input — it may
11+
// return an error, but it must not crash the server. The seed corpus runs on
12+
// every `go test`; `go test -fuzz=FuzzParse` explores further.
13+
func FuzzParse(f *testing.F) {
14+
seeds := []string{
15+
"",
16+
".entity with(domain='devops', name='devops.service')",
17+
".entity with(domain='a', name='b', query='x', topk=5, mode='vector')",
18+
".umodel with(kind='entity_set') | project domain,name | sort domain | limit 10",
19+
".entity_set with(domain='d', name='n', ids=['1']) | entity-call get_metrics('d','m','x', step='30s')",
20+
".topo | graph-call getNeighborNodes('full', 2, [(:\"d@n\" {__entity_id__: '1'})]) | limit 5",
21+
".topo | graph-call cypher(`MATCH (n) RETURN n LIMIT 1`)",
22+
"not a query",
23+
".entity with(",
24+
".entity with(domain='x'",
25+
".topo | graph-call getNeighborNodes(",
26+
"| | |",
27+
".entity with(domain='a', name='b') | project | sort | limit",
28+
}
29+
for _, s := range seeds {
30+
f.Add(s)
31+
}
32+
f.Fuzz(func(t *testing.T, query string) {
33+
// Must not panic. An error return is acceptable.
34+
_, _ = Parse(model.QueryRequest{Query: query})
35+
})
36+
}

internal/umodel/fuzz_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package umodel
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
"testing"
7+
8+
"github.com/alibaba/UnifiedModel/internal/graphstore"
9+
)
10+
11+
// FuzzConfineImportPath hardens the import-path confinement, the boundary that
12+
// stops an API caller from reading arbitrary server files. The security
13+
// invariant: if confineImportPath accepts a path (no error), the returned path
14+
// must stay inside the import root. The fuzzer hunts for adversarial inputs
15+
// (traversal, odd separators, NUL bytes) that get accepted yet escape.
16+
func FuzzConfineImportPath(f *testing.F) {
17+
const root = "/srv/umodel/import-root"
18+
svc := NewService(graphstore.NewMemoryStore(), WithImportRoot(root))
19+
rootAbs, err := filepath.Abs(root)
20+
if err != nil {
21+
f.Fatalf("abs root: %v", err)
22+
}
23+
rootAbs = filepath.Clean(rootAbs)
24+
25+
seeds := []string{
26+
root + "/pack",
27+
root + "/a/b/c",
28+
"pack",
29+
"a/b",
30+
"../etc/passwd",
31+
"/etc/passwd",
32+
"",
33+
".",
34+
"..",
35+
root + "/../escape",
36+
root + "/a/../../escape",
37+
"\x00",
38+
root + "/\x00pack",
39+
}
40+
for _, s := range seeds {
41+
f.Add(s)
42+
}
43+
44+
f.Fuzz(func(t *testing.T, p string) {
45+
out, err := svc.confineImportPath(p)
46+
if err != nil {
47+
return // rejected — safe
48+
}
49+
rel, relErr := filepath.Rel(rootAbs, out)
50+
if relErr != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
51+
t.Fatalf("confineImportPath(%q) was accepted but escapes the root: out=%q rel=%q", p, out, rel)
52+
}
53+
})
54+
}

0 commit comments

Comments
 (0)