Skip to content

Commit cefe427

Browse files
Enhance RPM audit functionality and improve evidence file uploads (#58)
- Added workspace directory resolution for RPM audit uploads in `packagePromote`. - Updated `UploadPromotionEvidenceFiles` to accept a workspace directory parameter for resolving relative paths to sibling RPM audit JSON files. - Implemented `rpmAuditPathsFromTestResults` to discover and append sibling audit files based on test results. - Introduced new tests for loading and writing RPM audit records, ensuring proper handling of audit data alongside built RPMs.
1 parent 813ab27 commit cefe427

6 files changed

Lines changed: 447 additions & 15 deletions

File tree

internal/cli/cli.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,12 @@ func packagePromote(c *cli.Context) error {
548548
releaseTagsByRuntime[runtimeName] = tag
549549
}
550550
}
551-
if err := UploadPromotionEvidenceFiles(uploader, releaseTagsByRuntime, testResultsPath); err != nil {
551+
workspaceDir, wdErr := os.Getwd()
552+
if wdErr != nil {
553+
stderr.Warn("failed to resolve working directory for RPM audit upload", "error", wdErr)
554+
workspaceDir = ""
555+
}
556+
if err := UploadPromotionEvidenceFiles(uploader, releaseTagsByRuntime, testResultsPath, workspaceDir); err != nil {
552557
stderr.Error("failed to upload promotion evidence files", "error", err)
553558
return err
554559
}

internal/cli/package_promotion.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,18 @@ func BuildPromotionUploader(configPath, token string, manifest PackageManifest)
140140
return &runtimeReleaseUploader{clientsByRuntime: clientsByRuntime}, nil
141141
}
142142

143+
// UploadPromotionEvidenceFiles uploads pipeline evidence files (manifests, results,
144+
// and sibling RPM audit JSONs) to each runtime's GitHub release.
145+
//
146+
// workspaceDir is used to resolve relative PackagePath values found inside the
147+
// test-results file when looking for sibling *.rpm.audit.json files. Pass the
148+
// process working directory (os.Getwd()) from the caller; an empty string
149+
// disables relative-path resolution.
143150
func UploadPromotionEvidenceFiles(
144151
uploader promotion.ReleaseAssetUploader,
145152
releaseTagsByRuntime map[string]string,
146153
testResultsPath string,
154+
workspaceDir string,
147155
) error {
148156
if uploader == nil {
149157
return nil
@@ -152,13 +160,17 @@ func UploadPromotionEvidenceFiles(
152160
return nil
153161
}
154162

155-
artifactsDir := filepath.Dir(strings.TrimSpace(testResultsPath))
163+
cleanResultsPath := strings.TrimSpace(testResultsPath)
164+
artifactsDir := filepath.Dir(cleanResultsPath)
156165
evidenceFiles := []string{
157-
strings.TrimSpace(testResultsPath),
166+
cleanResultsPath,
158167
filepath.Join(artifactsDir, "package-build-results.json"),
159168
filepath.Join(artifactsDir, "package-manifest.built.json"),
160169
filepath.Join(artifactsDir, "package-manifest.tested.json"),
161170
}
171+
// Append sibling *.rpm.audit.json files discovered from the test results.
172+
evidenceFiles = append(evidenceFiles, rpmAuditPathsFromTestResults(workspaceDir, cleanResultsPath)...)
173+
162174
seen := make(map[string]struct{})
163175
for _, runtimeName := range sortedRuntimeKeys(releaseTagsByRuntime) {
164176
releaseTag := releaseTagsByRuntime[runtimeName]
@@ -189,6 +201,49 @@ func UploadPromotionEvidenceFiles(
189201
return nil
190202
}
191203

204+
// rpmAuditPathsFromTestResults reads testResultsPath and returns the absolute
205+
// paths of any sibling *.rpm.audit.json files that exist on disk alongside the
206+
// built RPM packages recorded as passed in the test results.
207+
func rpmAuditPathsFromTestResults(workspaceDir, testResultsPath string) []string {
208+
if strings.TrimSpace(testResultsPath) == "" {
209+
return nil
210+
}
211+
data, err := os.ReadFile(testResultsPath)
212+
if err != nil {
213+
return nil
214+
}
215+
var results promotion.TestResultsFile
216+
if err := json.Unmarshal(data, &results); err != nil {
217+
return nil
218+
}
219+
220+
var paths []string
221+
seen := make(map[string]struct{})
222+
for _, r := range results.Results {
223+
if strings.ToLower(strings.TrimSpace(r.Target)) != "rpm" {
224+
continue
225+
}
226+
pkgPath := strings.TrimSpace(r.PackagePath)
227+
if pkgPath == "" {
228+
continue
229+
}
230+
if !filepath.IsAbs(pkgPath) && strings.TrimSpace(workspaceDir) != "" {
231+
pkgPath = filepath.Join(workspaceDir, pkgPath)
232+
}
233+
auditPath := pkgPath + ".audit.json"
234+
if _, ok := seen[auditPath]; ok {
235+
continue
236+
}
237+
seen[auditPath] = struct{}{}
238+
info, statErr := os.Stat(auditPath)
239+
if statErr != nil || info.IsDir() || info.Size() == 0 {
240+
continue
241+
}
242+
paths = append(paths, auditPath)
243+
}
244+
return paths
245+
}
246+
192247
func sortedRuntimeKeys(byRuntime map[string]string) []string {
193248
keys := make([]string, 0, len(byRuntime))
194249
for runtimeName := range byRuntime {

internal/cli/release.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,12 @@ func (rm *ReleaseManager) collectPackageArtifacts(outputDir, runtimeName, versio
383383
}
384384
if packageArtifactFilename(filepath.Base(fullPath)) {
385385
appendFile(fullPath, info)
386+
if strings.HasSuffix(strings.ToLower(filepath.Base(fullPath)), ".rpm") {
387+
auditPath := fullPath + ".audit.json"
388+
if ainfo, aerr := os.Stat(auditPath); aerr == nil && !ainfo.IsDir() && ainfo.Size() > 0 {
389+
appendFile(auditPath, ainfo)
390+
}
391+
}
386392
}
387393
}
388394

@@ -857,6 +863,17 @@ func getFileInfo(filename, url string, downloadResults []runtime.DownloadResult)
857863
}, nil
858864
}
859865

866+
// Sibling audit for an RPM (e.g. OSPO-nodejs-….rpm.audit.json) — same platform key as the .rpm binary.
867+
if strings.HasSuffix(strings.ToLower(filename), ".rpm.audit.json") {
868+
baseRPM := strings.TrimSuffix(filename, ".audit.json")
869+
return &fileInfo{
870+
Version: extractVersionFromFilename(baseRPM),
871+
OS: "linux",
872+
Arch: packageArchFromFilename(baseRPM),
873+
Type: "artifact",
874+
}, nil
875+
}
876+
860877
// Related verification/proof files may append known suffixes to the binary name.
861878
// Try resolving back to the original binary filename and map platform via downloadResults.
862879
baseFilename := filename

internal/packageexec/audit.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package packageexec
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"strings"
8+
"time"
9+
10+
"github.com/clean-dependency-project/cdprun/internal/packaging"
11+
)
12+
13+
// Phase 1 — load download audit (read-only).
14+
// Phase 2 — assemble build + test metadata after container runs.
15+
// Phase 3 — write sibling {rpm}.audit.json next to the .rpm.
16+
17+
// WriteRPMAuditRecord writes a single JSON file next to the built RPM containing
18+
// inlined download verification (from the upstream tarball's .audit.json),
19+
// build container metadata, and test container output.
20+
func WriteRPMAuditRecord(
21+
workspaceDir string,
22+
runID string,
23+
target Target,
24+
build packaging.BuildResult,
25+
execSpec TargetExecutorSpec,
26+
testStdout string,
27+
testStderr string,
28+
testErr error,
29+
) error {
30+
if strings.ToLower(strings.TrimSpace(target.Target)) != "rpm" {
31+
return fmt.Errorf("WriteRPMAuditRecord: target is not rpm")
32+
}
33+
rpmAbs, err := resolvePath(workspaceDir, build.PackagePath)
34+
if err != nil {
35+
return fmt.Errorf("resolve package path: %w", err)
36+
}
37+
auditPath := rpmAbs + ".audit.json"
38+
39+
downloadObj, err := loadDownloadAuditObject(workspaceDir, target.InputPath)
40+
if err != nil {
41+
downloadObj = fallbackDownloadObject(target)
42+
}
43+
44+
testStatus := "success"
45+
overallVer := "success"
46+
if testErr != nil {
47+
testStatus = "failed"
48+
overallVer = "failed"
49+
}
50+
51+
testOutput, errParse := parseTestOutputJSON(testStdout)
52+
testSection := map[string]interface{}{
53+
"image": execSpec.Test.Image,
54+
"script": execSpec.Test.Script,
55+
"status": testStatus,
56+
}
57+
if strings.TrimSpace(testStderr) != "" {
58+
testSection["stderr"] = strings.TrimSpace(testStderr)
59+
}
60+
if testErr != nil {
61+
testSection["error"] = testErr.Error()
62+
}
63+
if errParse == nil && testOutput != nil {
64+
testSection["output"] = testOutput
65+
} else if strings.TrimSpace(testStdout) != "" {
66+
testSection["output_raw"] = strings.TrimSpace(testStdout)
67+
}
68+
69+
record := map[string]interface{}{
70+
"timestamp": time.Now().UTC().Format(time.RFC3339),
71+
"run_id": runID,
72+
"runtime": target.Runtime,
73+
"version": target.Version,
74+
"package_type": "rpm",
75+
"package_name": build.PackageName,
76+
"package_filename": build.PackageFilename,
77+
"package_path": build.PackagePath,
78+
"package_sha256": build.PackageSHA256,
79+
"install_prefix": target.InstallPrefix,
80+
"arch": build.Arch,
81+
"release": build.Release,
82+
"download": downloadObj,
83+
"build": buildSection(execSpec.Build, build),
84+
"test": testSection,
85+
"verification_type": "package-build-test-rpm",
86+
"verification_status": overallVer,
87+
}
88+
89+
enc, err := json.MarshalIndent(record, "", " ")
90+
if err != nil {
91+
return fmt.Errorf("marshal rpm audit: %w", err)
92+
}
93+
if err := os.WriteFile(auditPath, enc, 0644); err != nil {
94+
return fmt.Errorf("write rpm audit file: %w", err)
95+
}
96+
return nil
97+
}
98+
99+
func buildSection(spec ContainerSpec, build packaging.BuildResult) map[string]interface{} {
100+
out := map[string]interface{}{
101+
"image": spec.Image,
102+
"script": spec.Script,
103+
"status": "success",
104+
}
105+
if build.Duration > 0 {
106+
out["duration_ms"] = build.Duration.Milliseconds()
107+
}
108+
return out
109+
}
110+
111+
func loadDownloadAuditObject(workspaceDir, inputPath string) (map[string]interface{}, error) {
112+
inputPath = strings.TrimSpace(inputPath)
113+
if inputPath == "" {
114+
return nil, fmt.Errorf("empty input path")
115+
}
116+
full, err := resolvePath(workspaceDir, inputPath)
117+
if err != nil {
118+
return nil, err
119+
}
120+
auditPath := full + ".audit.json"
121+
data, err := os.ReadFile(auditPath)
122+
if err != nil {
123+
return nil, err
124+
}
125+
var m map[string]interface{}
126+
if err := json.Unmarshal(data, &m); err != nil {
127+
return nil, err
128+
}
129+
return m, nil
130+
}
131+
132+
func fallbackDownloadObject(target Target) map[string]interface{} {
133+
return map[string]interface{}{
134+
"input_path": target.InputPath,
135+
"input_sha256": target.InputSHA256,
136+
"audit_file_missing": true,
137+
}
138+
}
139+
140+
func parseTestOutputJSON(stdout string) (map[string]interface{}, error) {
141+
s := strings.TrimSpace(stdout)
142+
if s == "" {
143+
return nil, fmt.Errorf("empty test stdout")
144+
}
145+
var m map[string]interface{}
146+
if err := json.Unmarshal([]byte(s), &m); err != nil {
147+
return nil, err
148+
}
149+
return m, nil
150+
}
151+
152+
// DownloadAuditPathForInput returns the path to the sibling audit file for an
153+
// input tarball (workspace-relative or absolute), for tests and tooling.
154+
func DownloadAuditPathForInput(workspaceDir, inputPath string) (string, error) {
155+
full, err := resolvePath(workspaceDir, inputPath)
156+
if err != nil {
157+
return "", err
158+
}
159+
return full + ".audit.json", nil
160+
}
161+
162+
// RPMAuditPath returns the RPM audit file path next to the given RPM path.
163+
func RPMAuditPath(rpmPath string) string {
164+
return rpmPath + ".audit.json"
165+
}
166+
167+
// IsRPMAuditFilename reports whether name is a sibling audit for an RPM
168+
// (e.g. OSPO-nodejs-22.22.2-1.amzn2023.x86_64.rpm.audit.json).
169+
func IsRPMAuditFilename(name string) bool {
170+
lower := strings.ToLower(strings.TrimSpace(name))
171+
return strings.HasSuffix(lower, ".rpm.audit.json")
172+
}

0 commit comments

Comments
 (0)