fix(ax): normalise audit and health machine output

This commit is contained in:
Virgil 2026-03-30 11:59:38 +00:00
parent 140d2b0583
commit 364b4b96de
5 changed files with 110 additions and 9 deletions

View file

@ -269,8 +269,15 @@ func summariseHealthResults(totalRepos int, filteredRepos int, results []RepoHea
summary := HealthSummary{
TotalRepos: totalRepos,
FilteredRepos: filteredRepos,
ByStatus: make(map[string]int),
ProblemsOnly: problemsOnly,
ByStatus: map[string]int{
"passing": 0,
"failing": 0,
"error": 0,
"pending": 0,
"disabled": 0,
"no_ci": 0,
},
ProblemsOnly: problemsOnly,
}
for _, health := range results {

View file

@ -424,35 +424,52 @@ func addPHPSecurityCommand(parent *cli.Command) {
}
type auditJSONOutput struct {
Results []AuditResultJSON `json:"results"`
Results []auditResultJSON `json:"results"`
HasVulnerabilities bool `json:"has_vulnerabilities"`
Vulnerabilities int `json:"vulnerabilities"`
}
type AuditResultJSON struct {
type auditResultJSON struct {
Tool string `json:"tool"`
Vulnerabilities int `json:"vulnerabilities"`
Advisories []php.AuditAdvisory `json:"advisories"`
Advisories []auditAdvisoryJSON `json:"advisories"`
Error string `json:"error,omitempty"`
}
type auditAdvisoryJSON struct {
Package string `json:"package"`
Severity string `json:"severity,omitempty"`
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"`
Identifiers []string `json:"identifiers,omitempty"`
}
func mapAuditResultsForJSON(results []php.AuditResult) auditJSONOutput {
output := auditJSONOutput{
Results: make([]AuditResultJSON, 0, len(results)),
Results: make([]auditResultJSON, 0, len(results)),
}
sort.Slice(results, func(i, j int) bool {
return results[i].Tool < results[j].Tool
})
for _, result := range results {
entry := AuditResultJSON{
entry := auditResultJSON{
Tool: result.Tool,
Vulnerabilities: result.Vulnerabilities,
Advisories: append([]php.AuditAdvisory(nil), result.Advisories...),
}
if result.Error != nil {
entry.Error = result.Error.Error()
}
entry.Advisories = make([]auditAdvisoryJSON, 0, len(result.Advisories))
for _, advisory := range result.Advisories {
entry.Advisories = append(entry.Advisories, auditAdvisoryJSON{
Package: advisory.Package,
Severity: advisory.Severity,
Title: advisory.Title,
URL: advisory.URL,
Identifiers: append([]string(nil), advisory.Identifiers...),
})
}
sort.Slice(entry.Advisories, func(i, j int) bool {
if entry.Advisories[i].Package == entry.Advisories[j].Package {
return entry.Advisories[i].Title < entry.Advisories[j].Title

View file

@ -153,6 +153,63 @@ func TestPHPSecuritySARIFOutput_IsStructuredAndChromeFree(t *testing.T) {
assert.NotContains(t, output, "Summary:")
}
func TestPHPAuditJSONOutput_UsesLowerCaseAdvisoryKeys(t *testing.T) {
dir := t.TempDir()
writeTestFile(t, filepath.Join(dir, "composer.json"), "{}")
writeExecutable(t, filepath.Join(dir, "composer"), `#!/bin/sh
cat <<'JSON'
{
"advisories": {
"vendor/package-a": [
{
"title": "Remote Code Execution",
"link": "https://example.com/advisory/1",
"cve": "CVE-2025-1234",
"affectedVersions": ">=1.0,<1.5"
}
]
}
}
JSON
`)
restoreWorkingDir(t, dir)
prependPath(t, dir)
resetPHPAuditFlags(t)
parent := &cli.Command{Use: "qa"}
addPHPAuditCommand(parent)
command := findSubcommand(t, parent, "audit")
require.NoError(t, command.Flags().Set("json", "true"))
var runErr error
output := captureStdout(t, func() {
runErr = command.RunE(command, nil)
})
require.Error(t, runErr)
var payload struct {
Results []struct {
Tool string `json:"tool"`
Advisories []struct {
Package string `json:"package"`
} `json:"advisories"`
} `json:"results"`
HasVulnerabilities bool `json:"has_vulnerabilities"`
Vulnerabilities int `json:"vulnerabilities"`
}
require.NoError(t, json.Unmarshal([]byte(output), &payload))
require.Len(t, payload.Results, 1)
assert.Equal(t, "composer", payload.Results[0].Tool)
require.Len(t, payload.Results[0].Advisories, 1)
assert.Equal(t, "vendor/package-a", payload.Results[0].Advisories[0].Package)
assert.True(t, payload.HasVulnerabilities)
assert.Equal(t, 1, payload.Vulnerabilities)
assert.NotContains(t, output, "\"Package\"")
assert.NotContains(t, output, "Dependency Audit")
}
func TestPHPTestJUnitOutput_PrintsOnlyXML(t *testing.T) {
dir := t.TempDir()
writeTestFile(t, filepath.Join(dir, "composer.json"), "{}")
@ -258,6 +315,18 @@ func resetPHPSecurityFlags(t *testing.T) {
})
}
func resetPHPAuditFlags(t *testing.T) {
t.Helper()
oldJSON := phpAuditJSON
oldFix := phpAuditFix
phpAuditJSON = false
phpAuditFix = false
t.Cleanup(func() {
phpAuditJSON = oldJSON
phpAuditFix = oldFix
})
}
func resetPHPTestFlags(t *testing.T) {
t.Helper()
oldParallel := phpTestParallel

View file

@ -1,10 +1,12 @@
package php
import (
"cmp"
"context"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
coreio "forge.lthn.ai/core/go-io"
@ -112,6 +114,12 @@ func RunSecurityChecks(ctx context.Context, opts SecurityOptions) (*SecurityResu
}
}
// Keep the check order stable for callers that consume the package result
// directly instead of going through the CLI layer.
slices.SortFunc(result.Checks, func(a, b SecurityCheck) int {
return cmp.Compare(a.ID, b.ID)
})
return result, nil
}

View file

@ -16,5 +16,5 @@ tasks:
run_capture_stdout 1 "$output" ../../bin/core qa audit --json
jq -e '.results[0].tool == "composer" and .results[0].vulnerabilities == 1' "$output" >/dev/null
jq -e '.has_vulnerabilities == true and .vulnerabilities == 1' "$output" >/dev/null
jq -e '.results[0].advisories[0].Package == "vendor/package-a"' "$output" >/dev/null
jq -e '.results[0].advisories[0].package == "vendor/package-a"' "$output" >/dev/null
EOF