@@ -20,6 +20,7 @@ import (
2020 "context"
2121 "encoding/json"
2222 "fmt"
23+ "os"
2324 "path/filepath"
2425 "time"
2526
@@ -31,98 +32,109 @@ import (
3132 log "github.com/sirupsen/logrus"
3233 "github.com/spf13/afero"
3334
34- "github.com/enterprise-contract/ec- cli/internal/applicationsnapshot"
35- "github.com/enterprise-contract/ec- cli/internal/evaluator"
35+ "github.com/conforma/ cli/internal/applicationsnapshot"
36+ "github.com/conforma/ cli/internal/evaluator"
3637)
3738
38- // FS is the filesystem used for all file operations in this package. It defaults to the OS filesystem but can be replaced for testing.
39- var FS afero.Fs = afero .NewOsFs ()
40-
41- // Predicate defines the structure of the per-image VSA predicate.
39+ // Predicate represents a Verification Summary Attestation (VSA) predicate.
4240type Predicate struct {
4341 ImageRef string `json:"imageRef"`
44- ValidationResult string `json:"validationResult"` // "passed" or "failed"
42+ ValidationResult string `json:"validationResult"`
4543 Timestamp string `json:"timestamp"`
4644 Verifier string `json:"verifier"`
4745 PolicySource string `json:"policySource"`
4846 Component map [string ]interface {} `json:"component"`
4947 RuleResults []evaluator.Result `json:"ruleResults"`
5048}
5149
52- // Options for VSA generation
53- // Extend as needed for more context
54- // (e.g., output dir, signing key, etc.)
55- type Options struct {
56- OutputDir string
57- SigningKeyPath string
50+ // Generator handles VSA predicate generation
51+ type Generator struct {}
52+
53+ // NewGenerator creates a new VSA predicate generator
54+ func NewGenerator () * Generator {
55+ return & Generator {}
5856}
5957
6058// GeneratePredicate creates a Predicate for a validated image/component.
61- func GeneratePredicate (ctx context.Context , report applicationsnapshot.Report , component applicationsnapshot.Component , opts Options ) (* Predicate , error ) {
62- log .Infof ("Generating VSA predicate for image: %s" , component .ContainerImage )
59+ func ( g * Generator ) GeneratePredicate (ctx context.Context , report applicationsnapshot.Report , comp applicationsnapshot.Component ) (* Predicate , error ) {
60+ log .Infof ("Generating VSA predicate for image: %s" , comp .ContainerImage )
6361
6462 // Compose the component info as a map
6563 componentInfo := map [string ]interface {}{
66- "name" : component .Name ,
67- "containerImage" : component .ContainerImage ,
68- "source" : component .Source ,
64+ "name" : comp .Name ,
65+ "containerImage" : comp .ContainerImage ,
66+ "source" : comp .Source ,
6967 }
7068
7169 // Compose rule results: combine violations, warnings, and successes
72- ruleResults := make ([]evaluator.Result , 0 , len (component .Violations )+ len (component .Warnings )+ len (component .Successes ))
73- ruleResults = append (ruleResults , component .Violations ... )
74- ruleResults = append (ruleResults , component .Warnings ... )
75- ruleResults = append (ruleResults , component .Successes ... )
70+ ruleResults := make ([]evaluator.Result , 0 , len (comp .Violations )+ len (comp .Warnings )+ len (comp .Successes ))
71+ ruleResults = append (ruleResults , comp .Violations ... )
72+ ruleResults = append (ruleResults , comp .Warnings ... )
73+ ruleResults = append (ruleResults , comp .Successes ... )
7674
7775 validationResult := "failed"
78- if component .Success {
76+ if comp .Success {
7977 validationResult = "passed"
8078 }
8179
8280 policySource := ""
83- if report .Policy .PublicKey != "" {
81+ if report .Policy .Name != "" {
8482 policySource = report .Policy .Name
8583 }
8684
8785 return & Predicate {
88- ImageRef : component .ContainerImage ,
86+ ImageRef : comp .ContainerImage ,
8987 ValidationResult : validationResult ,
9088 Timestamp : time .Now ().UTC ().Format (time .RFC3339 ),
91- Verifier : "Conforma " ,
89+ Verifier : "ec-cli " ,
9290 PolicySource : policySource ,
9391 Component : componentInfo ,
9492 RuleResults : ruleResults ,
9593 }, nil
9694}
9795
98- // WriteVSA writes the Predicate as a JSON file to the given path.
99- func WriteVSA (predicate * Predicate , path string ) error {
100- log .Infof ("Writing VSA to %s" , path )
96+ // Writer handles VSA file writing
97+ type Writer struct {
98+ FS afero.Fs // defaults to the package-level FS or afero.NewOsFs()
99+ TempDirPrefix string // defaults to "vsa-"
100+ FilePerm os.FileMode // defaults to 0600
101+ }
102+
103+ // NewWriter creates a new VSA file writer
104+ func NewWriter () * Writer {
105+ return & Writer {
106+ FS : afero .NewOsFs (),
107+ TempDirPrefix : "vsa-" ,
108+ FilePerm : 0o600 ,
109+ }
110+ }
111+
112+ // WriteVSA writes the Predicate as a JSON file to a temp directory and returns the path.
113+ func (w * Writer ) WriteVSA (predicate * Predicate ) (string , error ) {
114+ log .Infof ("Writing VSA for image: %s" , predicate .ImageRef )
115+
116+ // Serialize with indent
101117 data , err := json .MarshalIndent (predicate , "" , " " )
102118 if err != nil {
103- return fmt .Errorf ("failed to marshal VSA predicate: %w" , err )
104- }
105- if err := FS .MkdirAll (filepath .Dir (path ), 0755 ); err != nil {
106- return fmt .Errorf ("failed to create VSA output directory: %w" , err )
119+ return "" , fmt .Errorf ("failed to marshal VSA predicate: %w" , err )
107120 }
108- if err := afero .WriteFile (FS , path , data , 0600 ); err != nil {
109- log .Errorf ("Failed to write VSA file: %v" , err )
110- return fmt .Errorf ("failed to write VSA file: %w" , err )
121+
122+ // Create temp directory using the injected FS and prefix
123+ tempDir , err := afero .TempDir (w .FS , "" , w .TempDirPrefix )
124+ if err != nil {
125+ return "" , fmt .Errorf ("failed to create temp directory: %w" , err )
111126 }
112- return nil
113- }
114127
115- // For testability, allow dependency injection of key loader and sign function
116- // These types match the cosign APIs
128+ fullPath := filepath .Join (tempDir , "vsa.json" )
117129
118- type PrivateKeyLoader func (key []byte , pass []byte ) (signature.SignerVerifier , error )
119- type AttestationSigner func (ctx context.Context , signer signature.SignerVerifier , ref name.Reference , att oci.Signature , opts * cosign.CheckOpts ) (name.Digest , error )
130+ log .Infof ("Writing VSA file to %s" , fullPath )
131+ // Write file with injected FS and file-permissions
132+ if err := afero .WriteFile (w .FS , fullPath , data , w .FilePerm ); err != nil {
133+ log .Errorf ("Failed to write VSA file to %s: %v" , fullPath , err )
134+ return "" , fmt .Errorf ("failed to write VSA file: %w" , err )
135+ }
120136
121- // SignVSAOptions allows injection for testing
122- // If nil, defaults to production cosign implementations
123- type SignVSAOptions struct {
124- KeyLoader PrivateKeyLoader
125- SignFunc AttestationSigner
137+ return fullPath , nil
126138}
127139
128140// AttestationUploader is a function that uploads an attestation and returns a result string or error
@@ -147,15 +159,35 @@ func NoopUploader(ctx context.Context, att oci.Signature, location string) (stri
147159 return "" , nil
148160}
149161
150- // SignVSA signs the VSA file and returns an oci.Signature (does not upload)
151- func SignVSA (ctx context.Context , vsaPath , keyPath , imageRef string , opts ... SignVSAOptions ) (oci.Signature , error ) {
162+ type PrivateKeyLoader func (key []byte , pass []byte ) (signature.SignerVerifier , error )
163+ type AttestationSigner func (ctx context.Context , signer signature.SignerVerifier , ref name.Reference , att oci.Signature , opts * cosign.CheckOpts ) (name.Digest , error )
164+
165+ type Signer struct {
166+ FS afero.Fs // for reading the VSA file
167+ KeyLoader PrivateKeyLoader // injected loader
168+ SignFunc AttestationSigner // injected cosign API
169+ }
170+
171+ func NewSigner (fs afero.Fs , loader PrivateKeyLoader , signer AttestationSigner ) * Signer {
172+ return & Signer {
173+ FS : fs ,
174+ KeyLoader : loader ,
175+ SignFunc : signer ,
176+ }
177+ }
178+
179+ // Sign reads the file, loads the key, and returns the signature.
180+ func (s * Signer ) Sign (ctx context.Context , vsaPath , keyPath , imageRef string ) (oci.Signature , error ) {
152181 log .Infof ("Signing VSA for image: %s" , imageRef )
153- vsaData , err := afero .ReadFile (FS , vsaPath )
182+ vsaData , err := afero .ReadFile (s . FS , vsaPath )
154183 if err != nil {
155184 log .Errorf ("Failed to read VSA file: %v" , err )
156185 return nil , fmt .Errorf ("failed to read VSA file: %w" , err )
157186 }
158187 // TODO: Actually sign the attestation using cosign APIs. For now, just create the attestation object.
188+ // Example:
189+ // signer, err := s.KeyLoader( /* load key bytes from keyPath */ )
190+ // attestationSigned, err := s.SignFunc(ctx, signer, ref, att, nil)
159191 att , err := static .NewAttestation (vsaData )
160192 if err != nil {
161193 log .Errorf ("Failed to create attestation: %v" , err )
0 commit comments