Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,12 @@ func packagePromote(c *cli.Context) error {
releaseTagsByRuntime[runtimeName] = tag
}
}
if err := UploadPromotionEvidenceFiles(uploader, releaseTagsByRuntime, testResultsPath); err != nil {
workspaceDir, wdErr := os.Getwd()
if wdErr != nil {
stderr.Warn("failed to resolve working directory for RPM audit upload", "error", wdErr)
workspaceDir = ""
}
if err := UploadPromotionEvidenceFiles(uploader, releaseTagsByRuntime, testResultsPath, workspaceDir); err != nil {
stderr.Error("failed to upload promotion evidence files", "error", err)
return err
}
Expand Down
59 changes: 57 additions & 2 deletions internal/cli/package_promotion.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,18 @@ func BuildPromotionUploader(configPath, token string, manifest PackageManifest)
return &runtimeReleaseUploader{clientsByRuntime: clientsByRuntime}, nil
}

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

artifactsDir := filepath.Dir(strings.TrimSpace(testResultsPath))
cleanResultsPath := strings.TrimSpace(testResultsPath)
artifactsDir := filepath.Dir(cleanResultsPath)
evidenceFiles := []string{
strings.TrimSpace(testResultsPath),
cleanResultsPath,
filepath.Join(artifactsDir, "package-build-results.json"),
filepath.Join(artifactsDir, "package-manifest.built.json"),
filepath.Join(artifactsDir, "package-manifest.tested.json"),
}
// Append sibling *.rpm.audit.json files discovered from the test results.
evidenceFiles = append(evidenceFiles, rpmAuditPathsFromTestResults(workspaceDir, cleanResultsPath)...)

seen := make(map[string]struct{})
for _, runtimeName := range sortedRuntimeKeys(releaseTagsByRuntime) {
releaseTag := releaseTagsByRuntime[runtimeName]
Expand Down Expand Up @@ -189,6 +201,49 @@ func UploadPromotionEvidenceFiles(
return nil
}

// rpmAuditPathsFromTestResults reads testResultsPath and returns the absolute
// paths of any sibling *.rpm.audit.json files that exist on disk alongside the
// built RPM packages recorded as passed in the test results.
func rpmAuditPathsFromTestResults(workspaceDir, testResultsPath string) []string {
if strings.TrimSpace(testResultsPath) == "" {
return nil
}
data, err := os.ReadFile(testResultsPath)
if err != nil {
return nil
}
var results promotion.TestResultsFile
if err := json.Unmarshal(data, &results); err != nil {
return nil
}

var paths []string
seen := make(map[string]struct{})
for _, r := range results.Results {
if strings.ToLower(strings.TrimSpace(r.Target)) != "rpm" {
continue
}
pkgPath := strings.TrimSpace(r.PackagePath)
if pkgPath == "" {
continue
}
if !filepath.IsAbs(pkgPath) && strings.TrimSpace(workspaceDir) != "" {
pkgPath = filepath.Join(workspaceDir, pkgPath)
}
auditPath := pkgPath + ".audit.json"
if _, ok := seen[auditPath]; ok {
continue
}
seen[auditPath] = struct{}{}
info, statErr := os.Stat(auditPath)
if statErr != nil || info.IsDir() || info.Size() == 0 {
continue
}
paths = append(paths, auditPath)
}
return paths
}

func sortedRuntimeKeys(byRuntime map[string]string) []string {
keys := make([]string, 0, len(byRuntime))
for runtimeName := range byRuntime {
Expand Down
17 changes: 17 additions & 0 deletions internal/cli/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,12 @@ func (rm *ReleaseManager) collectPackageArtifacts(outputDir, runtimeName, versio
}
if packageArtifactFilename(filepath.Base(fullPath)) {
appendFile(fullPath, info)
if strings.HasSuffix(strings.ToLower(filepath.Base(fullPath)), ".rpm") {
auditPath := fullPath + ".audit.json"
if ainfo, aerr := os.Stat(auditPath); aerr == nil && !ainfo.IsDir() && ainfo.Size() > 0 {
appendFile(auditPath, ainfo)
}
}
}
}

Expand Down Expand Up @@ -857,6 +863,17 @@ func getFileInfo(filename, url string, downloadResults []runtime.DownloadResult)
}, nil
}

// Sibling audit for an RPM (e.g. OSPO-nodejs-….rpm.audit.json) — same platform key as the .rpm binary.
if strings.HasSuffix(strings.ToLower(filename), ".rpm.audit.json") {
baseRPM := strings.TrimSuffix(filename, ".audit.json")
return &fileInfo{
Version: extractVersionFromFilename(baseRPM),
OS: "linux",
Arch: packageArchFromFilename(baseRPM),
Type: "artifact",
}, nil
}

// Related verification/proof files may append known suffixes to the binary name.
// Try resolving back to the original binary filename and map platform via downloadResults.
baseFilename := filename
Expand Down
172 changes: 172 additions & 0 deletions internal/packageexec/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package packageexec

import (
"encoding/json"
"fmt"
"os"
"strings"
"time"

"github.com/clean-dependency-project/cdprun/internal/packaging"
)

// Phase 1 — load download audit (read-only).
// Phase 2 — assemble build + test metadata after container runs.
// Phase 3 — write sibling {rpm}.audit.json next to the .rpm.

// WriteRPMAuditRecord writes a single JSON file next to the built RPM containing
// inlined download verification (from the upstream tarball's .audit.json),
// build container metadata, and test container output.
func WriteRPMAuditRecord(
workspaceDir string,
runID string,
target Target,
build packaging.BuildResult,
execSpec TargetExecutorSpec,
testStdout string,
testStderr string,
testErr error,
) error {
if strings.ToLower(strings.TrimSpace(target.Target)) != "rpm" {
return fmt.Errorf("WriteRPMAuditRecord: target is not rpm")
}
rpmAbs, err := resolvePath(workspaceDir, build.PackagePath)
if err != nil {
return fmt.Errorf("resolve package path: %w", err)
}
auditPath := rpmAbs + ".audit.json"

downloadObj, err := loadDownloadAuditObject(workspaceDir, target.InputPath)
if err != nil {
downloadObj = fallbackDownloadObject(target)
}

testStatus := "success"
overallVer := "success"
if testErr != nil {
testStatus = "failed"
overallVer = "failed"
}

testOutput, errParse := parseTestOutputJSON(testStdout)
testSection := map[string]interface{}{
"image": execSpec.Test.Image,
"script": execSpec.Test.Script,
"status": testStatus,
}
if strings.TrimSpace(testStderr) != "" {
testSection["stderr"] = strings.TrimSpace(testStderr)
}
if testErr != nil {
testSection["error"] = testErr.Error()
}
if errParse == nil && testOutput != nil {
testSection["output"] = testOutput
} else if strings.TrimSpace(testStdout) != "" {
testSection["output_raw"] = strings.TrimSpace(testStdout)
}

record := map[string]interface{}{
"timestamp": time.Now().UTC().Format(time.RFC3339),
"run_id": runID,
"runtime": target.Runtime,
"version": target.Version,
"package_type": "rpm",
"package_name": build.PackageName,
"package_filename": build.PackageFilename,
"package_path": build.PackagePath,
"package_sha256": build.PackageSHA256,
"install_prefix": target.InstallPrefix,
"arch": build.Arch,
"release": build.Release,
"download": downloadObj,
"build": buildSection(execSpec.Build, build),
"test": testSection,
"verification_type": "package-build-test-rpm",
"verification_status": overallVer,
}

enc, err := json.MarshalIndent(record, "", " ")
if err != nil {
return fmt.Errorf("marshal rpm audit: %w", err)
}
if err := os.WriteFile(auditPath, enc, 0644); err != nil {
return fmt.Errorf("write rpm audit file: %w", err)
}
return nil
}

func buildSection(spec ContainerSpec, build packaging.BuildResult) map[string]interface{} {
out := map[string]interface{}{
"image": spec.Image,
"script": spec.Script,
"status": "success",
}
if build.Duration > 0 {
out["duration_ms"] = build.Duration.Milliseconds()
}
return out
}

func loadDownloadAuditObject(workspaceDir, inputPath string) (map[string]interface{}, error) {
inputPath = strings.TrimSpace(inputPath)
if inputPath == "" {
return nil, fmt.Errorf("empty input path")
}
full, err := resolvePath(workspaceDir, inputPath)
if err != nil {
return nil, err
}
auditPath := full + ".audit.json"
data, err := os.ReadFile(auditPath)
if err != nil {
return nil, err
}
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
return nil, err
}
return m, nil
}

func fallbackDownloadObject(target Target) map[string]interface{} {
return map[string]interface{}{
"input_path": target.InputPath,
"input_sha256": target.InputSHA256,
"audit_file_missing": true,
}
}

func parseTestOutputJSON(stdout string) (map[string]interface{}, error) {
s := strings.TrimSpace(stdout)
if s == "" {
return nil, fmt.Errorf("empty test stdout")
}
var m map[string]interface{}
if err := json.Unmarshal([]byte(s), &m); err != nil {
return nil, err
}
return m, nil
}

// DownloadAuditPathForInput returns the path to the sibling audit file for an
// input tarball (workspace-relative or absolute), for tests and tooling.
func DownloadAuditPathForInput(workspaceDir, inputPath string) (string, error) {
full, err := resolvePath(workspaceDir, inputPath)
if err != nil {
return "", err
}
return full + ".audit.json", nil
}

// RPMAuditPath returns the RPM audit file path next to the given RPM path.
func RPMAuditPath(rpmPath string) string {
return rpmPath + ".audit.json"
}

// IsRPMAuditFilename reports whether name is a sibling audit for an RPM
// (e.g. OSPO-nodejs-22.22.2-1.amzn2023.x86_64.rpm.audit.json).
func IsRPMAuditFilename(name string) bool {
lower := strings.ToLower(strings.TrimSpace(name))
return strings.HasSuffix(lower, ".rpm.audit.json")
}
Loading
Loading