@@ -23,9 +23,10 @@ const (
2323)
2424
2525var (
26- caseIDPattern = regexp .MustCompile (`^[a-z0-9]+(?:-[a-z0-9]+)*$` )
27- revisionPattern = regexp .MustCompile (`^[0-9a-f]{40}$` )
28- hashPattern = regexp .MustCompile (`^[0-9a-f]{64}$` )
26+ caseIDPattern = regexp .MustCompile (`^[a-z0-9]+(?:-[a-z0-9]+)*$` )
27+ revisionPattern = regexp .MustCompile (`^[0-9a-f]{40}$` )
28+ hashPattern = regexp .MustCompile (`^[0-9a-f]{64}$` )
29+ containerDigestPattern = regexp .MustCompile (`@sha256:[0-9a-f]{64}$` )
2930)
3031
3132type catalog struct {
@@ -68,7 +69,8 @@ func main() {
6869 if err != nil {
6970 exitError (err )
7071 }
71- loaded , err := loadCatalog (filepath .Join (root , filepath .FromSlash (* catalogPath )))
72+ resolvedCatalog := resolveCatalogPath (root , * catalogPath )
73+ loaded , err := loadCatalog (resolvedCatalog )
7274 if err != nil {
7375 exitError (err )
7476 }
@@ -109,6 +111,14 @@ func repositoryRoot() (string, error) {
109111 }
110112}
111113
114+ func resolveCatalogPath (root , catalogPath string ) string {
115+ resolved := filepath .FromSlash (catalogPath )
116+ if filepath .IsAbs (resolved ) {
117+ return resolved
118+ }
119+ return filepath .Join (root , resolved )
120+ }
121+
112122func loadCatalog (path string ) (catalog , error ) {
113123 file , err := os .Open (path )
114124 if err != nil {
@@ -175,15 +185,15 @@ func validateCase(root string, current evidenceCase) error {
175185 return errors .New ("title and area are required" )
176186 }
177187 switch current .EvidenceLevel {
178- case "deterministic" , "pinned-input" , "live-service" , "manual-assurance" :
188+ case "deterministic" , "pinned-input" , "live-service" , "manual-assurance" , "snapshot" :
179189 default :
180190 return fmt .Errorf ("unsupported evidence level %q" , current .EvidenceLevel )
181191 }
182192 if len (current .Inputs ) == 0 {
183193 return errors .New ("at least one input is required" )
184194 }
185195 for _ , item := range current .Inputs {
186- if err := validateInput (root , item ); err != nil {
196+ if err := validateInput (root , current . EvidenceLevel , item ); err != nil {
187197 return err
188198 }
189199 }
@@ -211,10 +221,20 @@ func validateCase(root string, current evidenceCase) error {
211221 if len (current .Proves ) == 0 || len (current .Limitations ) == 0 {
212222 return errors .New ("proves and limitations must both be explicit" )
213223 }
224+ for _ , claim := range current .Proves {
225+ if strings .TrimSpace (claim ) == "" {
226+ return errors .New ("proves and limitations cannot contain blank entries" )
227+ }
228+ }
229+ for _ , limitation := range current .Limitations {
230+ if strings .TrimSpace (limitation ) == "" {
231+ return errors .New ("proves and limitations cannot contain blank entries" )
232+ }
233+ }
214234 return nil
215235}
216236
217- func validateInput (root string , current input ) error {
237+ func validateInput (root , evidenceLevel string , current input ) error {
218238 if strings .TrimSpace (current .Location ) == "" {
219239 return errors .New ("input location is required" )
220240 }
@@ -232,6 +252,9 @@ func validateInput(root string, current input) error {
232252 if current .Ref == "" {
233253 return errors .New ("container input requires an image reference" )
234254 }
255+ if evidenceLevel == "pinned-input" && ! containerDigestPattern .MatchString (current .Ref ) {
256+ return errors .New ("pinned container input requires an immutable sha256 digest" )
257+ }
235258 case "workflow" :
236259 if ! hashPattern .MatchString (current .SHA256 ) {
237260 return errors .New ("workflow input requires a SHA-256 hash" )
@@ -251,15 +274,30 @@ func validateArtifact(root string, item artifact) error {
251274 if filepath .IsAbs (clean ) || clean == ".." || strings .HasPrefix (clean , ".." + string (filepath .Separator )) {
252275 return fmt .Errorf ("artifact path %q must stay inside the repository" , item .Path )
253276 }
277+ resolvedRoot , err := filepath .EvalSymlinks (root )
278+ if err != nil {
279+ return fmt .Errorf ("resolve repository root: %w" , err )
280+ }
254281 path := filepath .Join (root , clean )
255- info , err := os .Stat (path )
282+ resolvedPath , err := filepath .EvalSymlinks (path )
283+ if err != nil {
284+ return fmt .Errorf ("resolve artifact %q: %w" , item .Path , err )
285+ }
286+ relative , err := filepath .Rel (resolvedRoot , resolvedPath )
287+ if err != nil {
288+ return fmt .Errorf ("resolve artifact %q relative to repository: %w" , item .Path , err )
289+ }
290+ if relative == ".." || strings .HasPrefix (relative , ".." + string (filepath .Separator )) {
291+ return fmt .Errorf ("artifact path %q resolves outside the repository" , item .Path )
292+ }
293+ info , err := os .Stat (resolvedPath )
256294 if err != nil {
257295 return fmt .Errorf ("inspect artifact %q: %w" , item .Path , err )
258296 }
259297 if ! info .Mode ().IsRegular () {
260298 return fmt .Errorf ("artifact %q is not a regular file" , item .Path )
261299 }
262- data , err := os .ReadFile (path )
300+ data , err := os .ReadFile (resolvedPath )
263301 if err != nil {
264302 return fmt .Errorf ("read artifact %q: %w" , item .Path , err )
265303 }
0 commit comments