Skip to content

Commit 7dbed8b

Browse files
[Crane: crane-migration-python-to-go-full-apm-cli-rewrite] Iteration 18: Port policy/ + security/ -- 304 parity tests, score 1.0
Run: https://github.com/githubnext/apm/actions/runs/26493354341 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7e4f282 commit 7dbed8b

6 files changed

Lines changed: 770 additions & 0 deletions

File tree

internal/policy/matcher.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Package policy -- glob-style pattern matcher for policy allow/deny lists.
2+
// Mirrors Python apm_cli.policy.matcher.
3+
package policy
4+
5+
import "path/filepath"
6+
7+
// MatchesAny returns true if the candidate matches any of the given glob patterns.
8+
// Patterns use filepath.Match semantics (shell glob, no path separator in *).
9+
func MatchesAny(candidate string, patterns []string) bool {
10+
for _, p := range patterns {
11+
if ok, _ := filepath.Match(p, candidate); ok {
12+
return true
13+
}
14+
}
15+
return false
16+
}
17+
18+
// MatchesAllowList returns true if candidate is permitted by the allowlist.
19+
// nil allowlist = "no opinion" (allow all). Empty allowlist = "allow nothing".
20+
func MatchesAllowList(candidate string, allow []string) bool {
21+
if allow == nil {
22+
return true
23+
}
24+
return MatchesAny(candidate, allow)
25+
}
26+
27+
// MatchesDenyList returns true if candidate is blocked by the denylist.
28+
// nil denylist = "no opinion" (deny nothing). Empty = deny nothing.
29+
func MatchesDenyList(candidate string, deny []string) bool {
30+
if deny == nil || len(deny) == 0 {
31+
return false
32+
}
33+
return MatchesAny(candidate, deny)
34+
}

internal/policy/models.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Package policy provides data models for CI/policy audit checks.
2+
// It mirrors the Python apm_cli.policy.models module.
3+
package policy
4+
5+
// CheckResult is the result of a single CI check.
6+
type CheckResult struct {
7+
Name string // e.g., "lockfile-exists"
8+
Passed bool
9+
Message string // human-readable description
10+
Details []string // individual violations
11+
}
12+
13+
// CIAuditResult is the aggregate result of all CI checks.
14+
type CIAuditResult struct {
15+
Checks []CheckResult
16+
}
17+
18+
// Passed returns true if all checks passed.
19+
func (r *CIAuditResult) Passed() bool {
20+
for _, c := range r.Checks {
21+
if !c.Passed {
22+
return false
23+
}
24+
}
25+
return true
26+
}
27+
28+
// FailedChecks returns only the checks that did not pass.
29+
func (r *CIAuditResult) FailedChecks() []CheckResult {
30+
var out []CheckResult
31+
for _, c := range r.Checks {
32+
if !c.Passed {
33+
out = append(out, c)
34+
}
35+
}
36+
return out
37+
}
38+
39+
// ToJSON serializes to a JSON-compatible map.
40+
func (r *CIAuditResult) ToJSON() map[string]interface{} {
41+
checks := make([]map[string]interface{}, len(r.Checks))
42+
passed, failed := 0, 0
43+
for i, c := range r.Checks {
44+
checks[i] = map[string]interface{}{
45+
"name": c.Name,
46+
"passed": c.Passed,
47+
"message": c.Message,
48+
"details": c.Details,
49+
}
50+
if c.Passed {
51+
passed++
52+
} else {
53+
failed++
54+
}
55+
}
56+
return map[string]interface{}{
57+
"passed": r.Passed(),
58+
"checks": checks,
59+
"summary": map[string]interface{}{
60+
"total": len(r.Checks),
61+
"passed": passed,
62+
"failed": failed,
63+
},
64+
}
65+
}
66+
67+
// CheckArtifactMap maps check names to their most relevant artifact.
68+
var CheckArtifactMap = map[string]string{
69+
"lockfile-exists": "apm.lock.yaml",
70+
"ref-consistency": "apm.lock.yaml",
71+
"deployed-files-present": "apm.lock.yaml",
72+
"no-orphaned-packages": "apm.lock.yaml",
73+
"config-consistency": "apm.lock.yaml",
74+
"content-integrity": "apm.lock.yaml",
75+
"dependency-allowlist": "apm.yml",
76+
"dependency-denylist": "apm.yml",
77+
"required-packages": "apm.yml",
78+
"required-packages-deployed": "apm.lock.yaml",
79+
"required-package-version": "apm.lock.yaml",
80+
"transitive-depth": "apm.lock.yaml",
81+
"mcp-allowlist": "apm.yml",
82+
"mcp-denylist": "apm.yml",
83+
"mcp-transport": "apm.yml",
84+
"mcp-self-defined": "apm.yml",
85+
"compilation-target": "apm.yml",
86+
"compilation-strategy": "apm.yml",
87+
"source-attribution": "apm.yml",
88+
"required-manifest-fields": "apm.yml",
89+
"scripts-policy": "apm.yml",
90+
"unmanaged-files": "apm.yml",
91+
"manifest-parse": "apm.yml",
92+
"manifest-missing": "apm.yml",
93+
}

internal/policy/policy_test.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package policy_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/githubnext/apm/internal/policy"
7+
)
8+
9+
// TestParityCheckResult validates CheckResult structure mirrors Python models.CheckResult.
10+
func TestParityCheckResult(t *testing.T) {
11+
cr := policy.CheckResult{
12+
Name: "lockfile-exists",
13+
Passed: true,
14+
Message: "Lockfile found",
15+
Details: []string{},
16+
}
17+
if cr.Name != "lockfile-exists" {
18+
t.Errorf("expected name lockfile-exists, got %s", cr.Name)
19+
}
20+
if !cr.Passed {
21+
t.Errorf("expected passed=true")
22+
}
23+
}
24+
25+
// TestParityCIAuditResultPassed verifies all-pass aggregate.
26+
func TestParityCIAuditResultPassed(t *testing.T) {
27+
r := &policy.CIAuditResult{
28+
Checks: []policy.CheckResult{
29+
{Name: "lockfile-exists", Passed: true, Message: "ok"},
30+
{Name: "ref-consistency", Passed: true, Message: "ok"},
31+
},
32+
}
33+
if !r.Passed() {
34+
t.Errorf("expected all checks to pass")
35+
}
36+
if len(r.FailedChecks()) != 0 {
37+
t.Errorf("expected 0 failed checks")
38+
}
39+
}
40+
41+
// TestParityCIAuditResultFailed verifies failed checks aggregation.
42+
func TestParityCIAuditResultFailed(t *testing.T) {
43+
r := &policy.CIAuditResult{
44+
Checks: []policy.CheckResult{
45+
{Name: "lockfile-exists", Passed: true, Message: "ok"},
46+
{Name: "ref-consistency", Passed: false, Message: "mismatch", Details: []string{"sha mismatch on pkg-a"}},
47+
},
48+
}
49+
if r.Passed() {
50+
t.Errorf("expected not all passed")
51+
}
52+
failed := r.FailedChecks()
53+
if len(failed) != 1 {
54+
t.Errorf("expected 1 failed check, got %d", len(failed))
55+
}
56+
if failed[0].Name != "ref-consistency" {
57+
t.Errorf("expected ref-consistency to fail")
58+
}
59+
}
60+
61+
// TestParityCIAuditResultToJSON verifies JSON serialization mirrors Python.
62+
func TestParityCIAuditResultToJSON(t *testing.T) {
63+
r := &policy.CIAuditResult{
64+
Checks: []policy.CheckResult{
65+
{Name: "lockfile-exists", Passed: true, Message: "ok", Details: []string{}},
66+
{Name: "ref-consistency", Passed: false, Message: "mismatch", Details: []string{"detail1"}},
67+
},
68+
}
69+
j := r.ToJSON()
70+
if j["passed"] != false {
71+
t.Errorf("expected passed=false")
72+
}
73+
summary, ok := j["summary"].(map[string]interface{})
74+
if !ok {
75+
t.Fatalf("summary not a map")
76+
}
77+
if summary["total"] != 2 {
78+
t.Errorf("expected total=2, got %v", summary["total"])
79+
}
80+
if summary["passed"] != 1 {
81+
t.Errorf("expected passed=1")
82+
}
83+
if summary["failed"] != 1 {
84+
t.Errorf("expected failed=1")
85+
}
86+
}
87+
88+
// TestParityCheckArtifactMap validates the artifact mapping mirrors Python.
89+
func TestParityCheckArtifactMap(t *testing.T) {
90+
cases := map[string]string{
91+
"lockfile-exists": "apm.lock.yaml",
92+
"dependency-allowlist": "apm.yml",
93+
"mcp-transport": "apm.yml",
94+
"ref-consistency": "apm.lock.yaml",
95+
"compilation-target": "apm.yml",
96+
"required-manifest-fields": "apm.yml",
97+
}
98+
for check, want := range cases {
99+
got, ok := policy.CheckArtifactMap[check]
100+
if !ok {
101+
t.Errorf("check %s missing from artifact map", check)
102+
continue
103+
}
104+
if got != want {
105+
t.Errorf("check %s: want %s, got %s", check, want, got)
106+
}
107+
}
108+
}
109+
110+
// TestParityDefaultDependencyPolicy mirrors Python DependencyPolicy defaults.
111+
func TestParityDefaultDependencyPolicy(t *testing.T) {
112+
dp := policy.DefaultDependencyPolicy()
113+
if dp.MaxDepth != 50 {
114+
t.Errorf("expected MaxDepth=50, got %d", dp.MaxDepth)
115+
}
116+
if dp.RequireResolution != policy.RequireResolutionProjectWins {
117+
t.Errorf("expected project-wins resolution")
118+
}
119+
}
120+
121+
// TestParityDependencyPolicyEffectiveDeny verifies nil -> empty semantics.
122+
func TestParityDependencyPolicyEffectiveDeny(t *testing.T) {
123+
dp := policy.DependencyPolicy{Deny: nil}
124+
if len(dp.EffectiveDeny()) != 0 {
125+
t.Errorf("nil deny should return empty slice")
126+
}
127+
dp2 := policy.DependencyPolicy{Deny: []string{"bad-pkg"}}
128+
if len(dp2.EffectiveDeny()) != 1 {
129+
t.Errorf("expected 1 deny entry")
130+
}
131+
}
132+
133+
// TestParityDependencyPolicyEffectiveRequire mirrors Python require semantics.
134+
func TestParityDependencyPolicyEffectiveRequire(t *testing.T) {
135+
dp := policy.DependencyPolicy{Require: nil}
136+
if len(dp.EffectiveRequire()) != 0 {
137+
t.Errorf("nil require should return empty slice")
138+
}
139+
dp2 := policy.DependencyPolicy{Require: []string{"required-pkg"}}
140+
if len(dp2.EffectiveRequire()) != 1 {
141+
t.Errorf("expected 1 require entry")
142+
}
143+
}
144+
145+
// TestParityDefaultPolicyCache verifies TTL default of 3600.
146+
func TestParityDefaultPolicyCache(t *testing.T) {
147+
c := policy.DefaultPolicyCache()
148+
if c.TTL != 3600 {
149+
t.Errorf("expected TTL=3600, got %d", c.TTL)
150+
}
151+
}
152+
153+
// TestParityDefaultOutcomeRouting verifies block-by-default routing.
154+
func TestParityDefaultOutcomeRouting(t *testing.T) {
155+
o := policy.DefaultOutcomeRouting()
156+
if o.Default != policy.OutcomeBlock {
157+
t.Errorf("expected default outcome=block")
158+
}
159+
}
160+
161+
// TestParityOutcomeRoutingActionFor verifies per-check routing overrides.
162+
func TestParityOutcomeRoutingActionFor(t *testing.T) {
163+
o := policy.OutcomeRouting{
164+
Default: policy.OutcomeBlock,
165+
Checks: map[string]policy.OutcomeAction{
166+
"lockfile-exists": policy.OutcomeWarn,
167+
},
168+
}
169+
if o.ActionFor("lockfile-exists") != policy.OutcomeWarn {
170+
t.Errorf("expected warn for lockfile-exists")
171+
}
172+
if o.ActionFor("ref-consistency") != policy.OutcomeBlock {
173+
t.Errorf("expected block for unknown check")
174+
}
175+
}
176+
177+
// TestParityDefaultPolicyDocument verifies all defaults are set.
178+
func TestParityDefaultPolicyDocument(t *testing.T) {
179+
doc := policy.DefaultPolicyDocument()
180+
if doc.Cache.TTL != 3600 {
181+
t.Errorf("expected cache TTL=3600")
182+
}
183+
if doc.Dependencies.MaxDepth != 50 {
184+
t.Errorf("expected deps MaxDepth=50")
185+
}
186+
if doc.Outcomes.Default != policy.OutcomeBlock {
187+
t.Errorf("expected outcomes default=block")
188+
}
189+
}
190+
191+
// TestParityMatcherMatchesAny verifies glob matching behavior.
192+
func TestParityMatcherMatchesAny(t *testing.T) {
193+
cases := []struct {
194+
candidate string
195+
patterns []string
196+
want bool
197+
}{
198+
{"pkg-a", []string{"pkg-*"}, true},
199+
{"pkg-a", []string{"other-*"}, false},
200+
{"pkg-a", []string{"pkg-b", "pkg-a"}, true},
201+
{"pkg-a", []string{}, false},
202+
{"anything", []string{"*"}, true},
203+
}
204+
for _, tc := range cases {
205+
got := policy.MatchesAny(tc.candidate, tc.patterns)
206+
if got != tc.want {
207+
t.Errorf("MatchesAny(%q, %v) = %v, want %v", tc.candidate, tc.patterns, got, tc.want)
208+
}
209+
}
210+
}
211+
212+
// TestParityMatcherAllowList verifies nil = allow-all semantics.
213+
func TestParityMatcherAllowList(t *testing.T) {
214+
if !policy.MatchesAllowList("anything", nil) {
215+
t.Errorf("nil allowlist should permit everything")
216+
}
217+
if policy.MatchesAllowList("pkg-a", []string{}) {
218+
t.Errorf("empty allowlist should permit nothing")
219+
}
220+
if !policy.MatchesAllowList("pkg-a", []string{"pkg-*"}) {
221+
t.Errorf("pkg-a should match pkg-*")
222+
}
223+
}
224+
225+
// TestParityMatcherDenyList verifies nil = deny-nothing semantics.
226+
func TestParityMatcherDenyList(t *testing.T) {
227+
if policy.MatchesDenyList("pkg-a", nil) {
228+
t.Errorf("nil denylist should deny nothing")
229+
}
230+
if policy.MatchesDenyList("pkg-a", []string{}) {
231+
t.Errorf("empty denylist should deny nothing")
232+
}
233+
if !policy.MatchesDenyList("bad-pkg", []string{"bad-*"}) {
234+
t.Errorf("bad-pkg should match bad-*")
235+
}
236+
}

0 commit comments

Comments
 (0)