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
6 changes: 6 additions & 0 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ Enabled public-repository security controls:
- Secret scanning push protection.
- Dependabot security updates.

Core analyzer guardrails:

- `pkg/prmaven` tests exercise analysis without GitHub or GitLab provider tokens.
- Production core code must not import network/provider clients.
- Production core code must not read provider environment variables.

Expected empty surfaces during stabilization:

- GitHub Actions secrets.
Expand Down
4 changes: 3 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Package: `pkg/prmaven`.

Coverage includes:

- local-first regression guards that exercise core analysis without provider tokens;
- static guards against core network/provider-client imports and provider environment reads;
- Maven module discovery from `pom.xml`;
- Surefire report parsing;
- Failsafe report parsing;
Expand Down Expand Up @@ -114,7 +116,7 @@ GitHub Actions runs:
- coverage generation on Linux;
- a minimum total coverage gate of 70%.

The CI workflow is intentionally dependency-light. Core tests do not require Maven, network services, GitHub tokens, Docker, or external APIs.
The CI workflow is intentionally dependency-light. Core tests do not require Maven, network services, GitHub tokens, Docker, or external APIs. The `pkg/prmaven` local-first guard fails if production core code starts importing network/provider clients or reading provider environment variables.

## Contributor Expectations

Expand Down
109 changes: 109 additions & 0 deletions pkg/prmaven/local_first_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package prmaven

import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strconv"
"strings"
"testing"
)

func TestAnalyzeDoesNotRequireProviderEnvironment(t *testing.T) {
for _, name := range []string{
"GITHUB_TOKEN",
"GH_TOKEN",
"GITHUB_ACTIONS",
"GITLAB_TOKEN",
"GITLAB_PRIVATE_TOKEN",
"CI_JOB_TOKEN",
} {
t.Setenv(name, "")
}

report, err := Analyze(Options{ProjectDir: "../../demo/multi-module-failure"})
if err != nil {
t.Fatal(err)
}
if report.Summary.FindingCount != 2 {
t.Fatalf("finding count = %d, want 2", report.Summary.FindingCount)
}
}

func TestCorePackageDoesNotImportNetworkOrProviderClients(t *testing.T) {
forbiddenImports := map[string]bool{
"net": true,
"net/http": true,
"net/rpc": true,
"golang.org/x/oauth2": true,
"github.com/google/go-github": true,
"github.com/xanzy/go-gitlab": true,
"gitlab.com/gitlab-org/api/client-go": true,
}

forEachProductionFile(t, func(path string, file *ast.File) {
for _, spec := range file.Imports {
importPath, err := strconv.Unquote(spec.Path.Value)
if err != nil {
t.Fatalf("unquote import in %s: %v", path, err)
}
if forbiddenImports[importPath] {
t.Fatalf("%s imports %q; core analysis must stay provider-agnostic and no-network", path, importPath)
}
}
})
}

func TestCorePackageDoesNotReadProviderEnvironment(t *testing.T) {
forbiddenSelectors := map[string]bool{
"Getenv": true,
"LookupEnv": true,
"Environ": true,
"ExpandEnv": true,
}

forEachProductionFile(t, func(path string, file *ast.File) {
ast.Inspect(file, func(node ast.Node) bool {
selector, ok := node.(*ast.SelectorExpr)
if !ok || !forbiddenSelectors[selector.Sel.Name] {
return true
}
ident, ok := selector.X.(*ast.Ident)
if ok && ident.Name == "os" {
t.Fatalf("%s reads environment through os.%s; core analysis must not require provider tokens", path, selector.Sel.Name)
}
return true
})
})
}

func forEachProductionFile(t *testing.T, visit func(path string, file *ast.File)) {
t.Helper()

fileSet := token.NewFileSet()
err := filepath.WalkDir(".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() {
if entry.Name() == "testdata" {
return filepath.SkipDir
}
return nil
}
if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
return nil
}
file, err := parser.ParseFile(fileSet, path, nil, 0)
if err != nil {
return err
}
visit(path, file)
return nil
})
if err != nil {
t.Fatal(err)
}
}
Loading