mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 17:57:54 -10:00
* ci: harden workflow permissions, add timeouts, concurrency and coverage report * ci: reuse make fmt-check and refine coverage table formatting * ci: scan Windows build graph via vulncheck-windows and collapse coverage summary to single-pass awk * ci: invoke the pinned Windows govulncheck via a local GOBIN go install writes the binary to GOBIN, which is not necessarily on PATH. Install into .cache/gobin and run that executable. GOOS=windows applies only to the scan so the scanner stays a host binary. * ci: declare contents:read on the action smoke workflow Pull-request workflows inherit the default GITHUB_TOKEN unless they set an explicit permission block. The smoke workflow is now covered by the same least-privilege policy as ci.yml. * ci: parse workflow permissions structurally and quote the Windows scanner path The pull-request token check now ignores commented lookalikes and accepts an inline on: [pull_request] trigger. vulncheck-windows keeps the cache path as one shell word. * ci: drop -race from this PR so issue 939 stays with PR 940 Coverage profile remains. The dedicated race-detector job is owned by the open race PR. * ci: parse workflow permissions as YAML and scope GOWORK=off * ci: label the non-race smoke test explicitly --------- Co-authored-by: cl-ment <[email protected]>
185 lines
4.4 KiB
Go
185 lines
4.4 KiB
Go
package installtest
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func TestPullRequestWorkflowsDeclareContentsRead(t *testing.T) {
|
|
root := filepath.Join(repoRoot(t), ".github", "workflows")
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var missing []string
|
|
for _, e := range entries {
|
|
if e.IsDir() || !(strings.HasSuffix(e.Name(), ".yml") || strings.HasSuffix(e.Name(), ".yaml")) {
|
|
continue
|
|
}
|
|
rel := filepath.Join(".github", "workflows", e.Name())
|
|
body := readRepoText(t, rel)
|
|
doc, err := parseWorkflow(body)
|
|
if err != nil {
|
|
t.Errorf("%s: yaml: %v", rel, err)
|
|
continue
|
|
}
|
|
if !workflowOnHasPullRequest(doc.On) {
|
|
continue
|
|
}
|
|
if !workflowPermissionsContentsRead(doc.Permissions) {
|
|
missing = append(missing, rel)
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
t.Fatalf("pull_request workflows missing permissions contents: read: %s", strings.Join(missing, ", "))
|
|
}
|
|
}
|
|
|
|
func TestWorkflowPermissionParsing(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
yml string
|
|
wantPR bool
|
|
wantPerm bool
|
|
}{
|
|
{
|
|
name: "mapping trigger",
|
|
yml: "on:\n pull_request:\npermissions:\n contents: read\n",
|
|
wantPR: true,
|
|
wantPerm: true,
|
|
},
|
|
{
|
|
name: "inline trigger list",
|
|
yml: "on: [pull_request]\npermissions:\n contents: read\n",
|
|
wantPR: true,
|
|
wantPerm: true,
|
|
},
|
|
{
|
|
name: "commented permissions ignored",
|
|
yml: "on:\n pull_request:\n# permissions:\n# contents: read\n",
|
|
wantPR: true,
|
|
wantPerm: false,
|
|
},
|
|
{
|
|
name: "nested script text is not a trigger",
|
|
yml: "on:\n push:\njobs:\n x:\n steps:\n - run: echo pull_request: permissions: contents: read\n",
|
|
wantPR: false,
|
|
wantPerm: false,
|
|
},
|
|
{
|
|
name: "inline comment does not fake contents read",
|
|
yml: "on: [pull_request]\npermissions: write-all # contents: read\n",
|
|
wantPR: true,
|
|
wantPerm: false,
|
|
},
|
|
{
|
|
name: "empty permissions mapping",
|
|
yml: "on: [pull_request]\npermissions: {}\n",
|
|
wantPR: true,
|
|
wantPerm: false,
|
|
},
|
|
{
|
|
name: "write-all is not contents read",
|
|
yml: "on: [pull_request]\npermissions: write-all\n",
|
|
wantPR: true,
|
|
wantPerm: false,
|
|
},
|
|
{
|
|
name: "quoted on and permissions keys",
|
|
yml: "\"on\": [pull_request]\n\"permissions\":\n \"contents\": read\n",
|
|
wantPR: true,
|
|
wantPerm: true,
|
|
},
|
|
{
|
|
name: "nested job permissions are not top-level",
|
|
yml: "on:\n pull_request:\njobs:\n x:\n permissions:\n contents: read\n",
|
|
wantPR: true,
|
|
wantPerm: false,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := workflowHasPullRequestTrigger(tc.yml); got != tc.wantPR {
|
|
t.Fatalf("trigger = %v, want %v", got, tc.wantPR)
|
|
}
|
|
if got := workflowHasTopLevelContentsRead(tc.yml); got != tc.wantPerm {
|
|
t.Fatalf("permissions = %v, want %v", got, tc.wantPerm)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVulncheckWindowsQuotesGobinPath(t *testing.T) {
|
|
body := readRepoText(t, "Makefile")
|
|
for _, want := range []string{
|
|
`mkdir -p "$(CURDIR)/.cache/gobin"`,
|
|
`GOBIN="$(CURDIR)/.cache/gobin"`,
|
|
`"$(CURDIR)/.cache/gobin/govulncheck"`,
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("vulncheck-windows missing quoted path %s", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
type workflowFile struct {
|
|
On any `yaml:"on"`
|
|
Permissions any `yaml:"permissions"`
|
|
}
|
|
|
|
func parseWorkflow(body string) (workflowFile, error) {
|
|
var doc workflowFile
|
|
err := yaml.Unmarshal([]byte(body), &doc)
|
|
return doc, err
|
|
}
|
|
|
|
func workflowHasPullRequestTrigger(body string) bool {
|
|
doc, err := parseWorkflow(body)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return workflowOnHasPullRequest(doc.On)
|
|
}
|
|
|
|
func workflowHasTopLevelContentsRead(body string) bool {
|
|
doc, err := parseWorkflow(body)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return workflowPermissionsContentsRead(doc.Permissions)
|
|
}
|
|
|
|
func workflowOnHasPullRequest(on any) bool {
|
|
switch v := on.(type) {
|
|
case string:
|
|
return v == "pull_request"
|
|
case []any:
|
|
for _, item := range v {
|
|
if workflowOnHasPullRequest(item) {
|
|
return true
|
|
}
|
|
}
|
|
case map[string]any:
|
|
_, ok := v["pull_request"]
|
|
return ok
|
|
}
|
|
return false
|
|
}
|
|
|
|
func workflowPermissionsContentsRead(perm any) bool {
|
|
m, ok := perm.(map[string]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
contents, ok := m["contents"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
s, ok := contents.(string)
|
|
return ok && s == "read"
|
|
}
|