Skip to content

Commit 1c7f7ad

Browse files
ralphbeanclaude
andcommitted
add --format json support for verifycommit and audit commands
This change adds machine-readable JSON output to the verifycommit and audit commands, making it easier to extract data programmatically (e.g., with jq) instead of parsing text output with bash. Changes: - Add common OutputFormat framework (internal/cmd/output.go) - Add --format flag to verifycommit command with JSON support - Add --format flag to audit command with JSON support - Include comprehensive test coverage for both commands The JSON output includes: - verifycommit: success status, commit info, verified SLSA levels - audit: commit results with summary statistics All tests pass and the implementation maintains backward compatibility with existing text output (default format). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 134593d commit 1c7f7ad

File tree

5 files changed

+807
-8
lines changed

5 files changed

+807
-8
lines changed

internal/cmd/audit.go

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,43 @@ func (e *AuditMode) Type() string {
5656
type auditOpts struct {
5757
branchOptions
5858
verifierOptions
59+
outputOptions
5960
auditDepth int
6061
endingCommit string
6162
auditMode AuditMode
6263
}
6364

65+
// AuditCommitResultJSON represents a single commit audit result in JSON format
66+
type AuditCommitResultJSON struct {
67+
Commit string `json:"commit"`
68+
Status string `json:"status"`
69+
VerifiedLevels []string `json:"verified_levels,omitempty"`
70+
PrevCommitMatches *bool `json:"prev_commit_matches,omitempty"`
71+
ProvControls interface{} `json:"prov_controls,omitempty"`
72+
GhControls interface{} `json:"gh_controls,omitempty"`
73+
PrevCommit string `json:"prev_commit,omitempty"`
74+
GhPriorCommit string `json:"gh_prior_commit,omitempty"`
75+
Link string `json:"link,omitempty"`
76+
Error string `json:"error,omitempty"`
77+
}
78+
79+
// AuditResultJSON represents the full audit result in JSON format
80+
type AuditResultJSON struct {
81+
Owner string `json:"owner"`
82+
Repository string `json:"repository"`
83+
Branch string `json:"branch"`
84+
LatestCommit string `json:"latest_commit"`
85+
CommitResults []AuditCommitResultJSON `json:"commit_results"`
86+
Summary *AuditSummary `json:"summary,omitempty"`
87+
}
88+
89+
// AuditSummary provides summary statistics for the audit
90+
type AuditSummary struct {
91+
TotalCommits int `json:"total_commits"`
92+
PassedCommits int `json:"passed_commits"`
93+
FailedCommits int `json:"failed_commits"`
94+
}
95+
6496
func (ao *auditOpts) Validate() error {
6597
errs := []error{
6698
ao.branchOptions.Validate(),
@@ -76,6 +108,8 @@ func (ao *auditOpts) AddFlags(cmd *cobra.Command) {
76108
cmd.PersistentFlags().StringVar(&ao.endingCommit, "ending-commit", "", "The commit to stop auditing at.")
77109
ao.auditMode = AuditModeBasic
78110
cmd.PersistentFlags().Var(&ao.auditMode, "audit-mode", "'basic' for limited details (default), 'full' for all details")
111+
ao.format = OutputFormatText
112+
cmd.PersistentFlags().Var(&ao.format, "format", "Output format: 'text' (default) or 'json'")
79113
}
80114

81115
func addAudit(parentCmd *cobra.Command) {
@@ -156,6 +190,41 @@ func printResult(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, m
156190
fmt.Printf("\tlink: https://github.com/%s/%s/commit/%s\n", ghc.Owner(), ghc.Repo(), ar.GhPriorCommit)
157191
}
158192

193+
func convertAuditResultToJSON(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, mode AuditMode) AuditCommitResultJSON {
194+
good := ar.IsGood()
195+
status := "passed"
196+
if !good {
197+
status = "failed"
198+
}
199+
200+
result := AuditCommitResultJSON{
201+
Commit: ar.Commit,
202+
Status: status,
203+
Link: fmt.Sprintf("https://github.com/%s/%s/commit/%s", ghc.Owner(), ghc.Repo(), ar.GhPriorCommit),
204+
}
205+
206+
// Only include details if mode is Full or status is failed
207+
if mode == AuditModeFull || !good {
208+
if ar.VsaPred != nil {
209+
result.VerifiedLevels = ar.VsaPred.GetVerifiedLevels()
210+
}
211+
212+
if ar.ProvPred != nil {
213+
result.ProvControls = ar.ProvPred.GetControls()
214+
result.PrevCommit = ar.ProvPred.GetPrevCommit()
215+
result.GhPriorCommit = ar.GhPriorCommit
216+
matches := ar.ProvPred.GetPrevCommit() == ar.GhPriorCommit
217+
result.PrevCommitMatches = &matches
218+
}
219+
220+
if ar.GhControlStatus != nil {
221+
result.GhControls = ar.GhControlStatus.Controls
222+
}
223+
}
224+
225+
return result
226+
}
227+
159228
func doAudit(auditArgs *auditOpts) error {
160229
ghc := ghcontrol.NewGhConnection(auditArgs.owner, auditArgs.repository, ghcontrol.BranchToFullRef(auditArgs.branch)).WithAuthToken(githubToken)
161230
ctx := context.Background()
@@ -169,23 +238,70 @@ func doAudit(auditArgs *auditOpts) error {
169238
return fmt.Errorf("could not get latest commit for %s", auditArgs.branch)
170239
}
171240

172-
fmt.Printf("Auditing branch %s starting from revision %s\n", auditArgs.branch, latestCommit)
241+
// For JSON output, collect all results
242+
if auditArgs.isJSON() {
243+
jsonResult := AuditResultJSON{
244+
Owner: auditArgs.owner,
245+
Repository: auditArgs.repository,
246+
Branch: auditArgs.branch,
247+
LatestCommit: latestCommit,
248+
CommitResults: []AuditCommitResultJSON{},
249+
}
250+
251+
count := 0
252+
passed := 0
253+
failed := 0
254+
255+
for ar, err := range auditor.AuditBranch(ctx, auditArgs.branch) {
256+
if ar == nil {
257+
return err
258+
}
259+
commitResult := convertAuditResultToJSON(ghc, ar, auditArgs.auditMode)
260+
if err != nil {
261+
commitResult.Error = err.Error()
262+
}
263+
if commitResult.Status == "passed" {
264+
passed++
265+
} else {
266+
failed++
267+
}
268+
jsonResult.CommitResults = append(jsonResult.CommitResults, commitResult)
269+
if auditArgs.endingCommit != "" && auditArgs.endingCommit == ar.Commit {
270+
break
271+
}
272+
if auditArgs.auditDepth > 0 && count >= auditArgs.auditDepth {
273+
break
274+
}
275+
count++
276+
}
277+
278+
jsonResult.Summary = &AuditSummary{
279+
TotalCommits: len(jsonResult.CommitResults),
280+
PassedCommits: passed,
281+
FailedCommits: failed,
282+
}
283+
284+
return auditArgs.writeJSON(jsonResult)
285+
}
286+
287+
// Text output (original behavior)
288+
auditArgs.writeText("Auditing branch %s starting from revision %s\n", auditArgs.branch, latestCommit)
173289

174290
count := 0
175291
for ar, err := range auditor.AuditBranch(ctx, auditArgs.branch) {
176292
if ar == nil {
177293
return err
178294
}
179295
if err != nil {
180-
fmt.Printf("\terror: %v\n", err)
296+
auditArgs.writeText("\terror: %v\n", err)
181297
}
182298
printResult(ghc, ar, auditArgs.auditMode)
183299
if auditArgs.endingCommit != "" && auditArgs.endingCommit == ar.Commit {
184-
fmt.Printf("Found ending commit %s\n", auditArgs.endingCommit)
300+
auditArgs.writeText("Found ending commit %s\n", auditArgs.endingCommit)
185301
return nil
186302
}
187303
if auditArgs.auditDepth > 0 && count >= auditArgs.auditDepth {
188-
fmt.Printf("Reached depth limit %d\n", auditArgs.auditDepth)
304+
auditArgs.writeText("Reached depth limit %d\n", auditArgs.auditDepth)
189305
return nil
190306
}
191307
count++

0 commit comments

Comments
 (0)