Skip to content
Merged
124 changes: 120 additions & 4 deletions internal/cmd/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,43 @@
type auditOpts struct {
branchOptions
verifierOptions
outputOptions
auditDepth int
endingCommit string
auditMode AuditMode
}

// AuditCommitResultJSON represents a single commit audit result in JSON format
type AuditCommitResultJSON struct {
Commit string `json:"commit"`

Check failure on line 67 in internal/cmd/audit.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
Status string `json:"status"`
VerifiedLevels []string `json:"verified_levels,omitempty"`
PrevCommitMatches *bool `json:"prev_commit_matches,omitempty"`
ProvControls interface{} `json:"prov_controls,omitempty"`
GhControls interface{} `json:"gh_controls,omitempty"`
PrevCommit string `json:"prev_commit,omitempty"`
GhPriorCommit string `json:"gh_prior_commit,omitempty"`
Link string `json:"link,omitempty"`
Error string `json:"error,omitempty"`
}

// AuditResultJSON represents the full audit result in JSON format
type AuditResultJSON struct {
Owner string `json:"owner"`
Repository string `json:"repository"`
Branch string `json:"branch"`
LatestCommit string `json:"latest_commit"`
CommitResults []AuditCommitResultJSON `json:"commit_results"`
Summary *AuditSummary `json:"summary,omitempty"`
}

// AuditSummary provides summary statistics for the audit
type AuditSummary struct {
TotalCommits int `json:"total_commits"`
PassedCommits int `json:"passed_commits"`
FailedCommits int `json:"failed_commits"`
}

func (ao *auditOpts) Validate() error {
errs := []error{
ao.branchOptions.Validate(),
Expand All @@ -76,6 +108,8 @@
cmd.PersistentFlags().StringVar(&ao.endingCommit, "ending-commit", "", "The commit to stop auditing at.")
ao.auditMode = AuditModeBasic
cmd.PersistentFlags().Var(&ao.auditMode, "audit-mode", "'basic' for limited details (default), 'full' for all details")
ao.format = OutputFormatText
cmd.PersistentFlags().Var(&ao.format, "format", "Output format: 'text' (default) or 'json'")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, since we are introducing a new output options set , move the flag initialization to an AddFlags() method and call the other embedded lines above:

ao.branchOptions.AddFlags(cmd)
ao.verifierOptions.AddFlags(cmd)

Also, simplify this call to a single one using StringVar() (see comment on the output options about turning this into a simple string):

Suggested change
ao.format = OutputFormatText
cmd.PersistentFlags().Var(&ao.format, "format", "Output format: 'text' (default) or 'json'")
ao.format = OutputFormatText
cmd.PersistentFlags().Var(&ao.format, "format", "Output format: 'text' (default) or 'json'")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 826f7aa and 503729f.

}

func addAudit(parentCmd *cobra.Command) {
Expand Down Expand Up @@ -123,7 +157,7 @@

func printResult(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, mode AuditMode) {
good := ar.IsGood()
status := "passed"

Check failure on line 160 in internal/cmd/audit.go

View workflow job for this annotation

GitHub Actions / lint

string `passed` has 3 occurrences, make it a constant (goconst)
if !good {
status = "failed"
}
Expand Down Expand Up @@ -156,6 +190,41 @@
fmt.Printf("\tlink: https://github.com/%s/%s/commit/%s\n", ghc.Owner(), ghc.Repo(), ar.GhPriorCommit)
}

func convertAuditResultToJSON(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, mode AuditMode) AuditCommitResultJSON {
good := ar.IsGood()
status := "passed"
if !good {
status = "failed"
}

result := AuditCommitResultJSON{
Commit: ar.Commit,
Status: status,
Link: fmt.Sprintf("https://github.com/%s/%s/commit/%s", ghc.Owner(), ghc.Repo(), ar.GhPriorCommit),
}

// Only include details if mode is Full or status is failed
if mode == AuditModeFull || !good {
if ar.VsaPred != nil {
result.VerifiedLevels = ar.VsaPred.GetVerifiedLevels()
}

if ar.ProvPred != nil {
result.ProvControls = ar.ProvPred.GetControls()
result.PrevCommit = ar.ProvPred.GetPrevCommit()
result.GhPriorCommit = ar.GhPriorCommit
matches := ar.ProvPred.GetPrevCommit() == ar.GhPriorCommit
result.PrevCommitMatches = &matches
}

if ar.GhControlStatus != nil {
result.GhControls = ar.GhControlStatus.Controls
}
}

return result
}

func doAudit(auditArgs *auditOpts) error {
ghc := ghcontrol.NewGhConnection(auditArgs.owner, auditArgs.repository, ghcontrol.BranchToFullRef(auditArgs.branch)).WithAuthToken(githubToken)
ctx := context.Background()
Expand All @@ -169,23 +238,70 @@
return fmt.Errorf("could not get latest commit for %s", auditArgs.branch)
}

fmt.Printf("Auditing branch %s starting from revision %s\n", auditArgs.branch, latestCommit)
// For JSON output, collect all results
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely comfortable with this arrangement. If I'm reading it properly we've duplicated the audit logic with the textual output, that could lead them to diverge in the future.

Suggestion:

Instead of duplicating the logic, merge them by adding a 'processResult' function that either

  1. calls printResult
  2. calls convertAuditResultToJson & appends it to jsonResult.CommitResults.

Then, at the end of the process you can auditArgs.writeJSON(jsonResult) or similar as needed.

Some of the enhancements you made (like the audit summary) might be interesting for the text based output too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 880805e.

if auditArgs.isJSON() {
jsonResult := AuditResultJSON{
Owner: auditArgs.owner,
Repository: auditArgs.repository,
Branch: auditArgs.branch,
LatestCommit: latestCommit,
CommitResults: []AuditCommitResultJSON{},
}

count := 0
passed := 0
failed := 0

for ar, err := range auditor.AuditBranch(ctx, auditArgs.branch) {
if ar == nil {
return err
}
commitResult := convertAuditResultToJSON(ghc, ar, auditArgs.auditMode)
if err != nil {
commitResult.Error = err.Error()
}
if commitResult.Status == "passed" {
passed++
} else {
failed++
}
jsonResult.CommitResults = append(jsonResult.CommitResults, commitResult)
if auditArgs.endingCommit != "" && auditArgs.endingCommit == ar.Commit {
break
}
if auditArgs.auditDepth > 0 && count >= auditArgs.auditDepth {
break
}
count++
}

jsonResult.Summary = &AuditSummary{
TotalCommits: len(jsonResult.CommitResults),
PassedCommits: passed,
FailedCommits: failed,
}

return auditArgs.writeJSON(jsonResult)
}

// Text output (original behavior)
auditArgs.writeText("Auditing branch %s starting from revision %s\n", auditArgs.branch, latestCommit)

count := 0
for ar, err := range auditor.AuditBranch(ctx, auditArgs.branch) {
if ar == nil {
return err
}
if err != nil {
fmt.Printf("\terror: %v\n", err)
auditArgs.writeText("\terror: %v\n", err)
}
printResult(ghc, ar, auditArgs.auditMode)
if auditArgs.endingCommit != "" && auditArgs.endingCommit == ar.Commit {
fmt.Printf("Found ending commit %s\n", auditArgs.endingCommit)
auditArgs.writeText("Found ending commit %s\n", auditArgs.endingCommit)
return nil
}
if auditArgs.auditDepth > 0 && count >= auditArgs.auditDepth {
fmt.Printf("Reached depth limit %d\n", auditArgs.auditDepth)
auditArgs.writeText("Reached depth limit %d\n", auditArgs.auditDepth)
return nil
}
count++
Expand Down
Loading
Loading