Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
37 changes: 17 additions & 20 deletions app/cli/pkg/action/attestation_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,20 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa
}
if format != materials.ArchiveNone {
if len(policyInputFiles) > 0 {
action.Logger.Warn().Msg("--policy-input-from-file is ignored when expanding an archive; evidence cross-links are not recorded for exploded materials")
// The runtime inputs still apply to every exploded material's policy
// evaluation (they flow through addOpts); only the per-input EVIDENCE
// materials are not recorded on the explode path.
action.Logger.Warn().Msg("--policy-input-from-file values apply to policy evaluation but are not recorded as evidence materials when expanding an archive")
}
limits := materials.ArchiveLimits{MaxEntries: action.maxExtractEntries, MaxTotalSize: action.maxExtractSize}
mts, err := crafter.AddMaterialsFromArchive(ctx, attestationID, materialType, materialName, materialValue, format, casBackend, annotations, limits, addOpts...)
// AddMaterialsFromArchive also records the source archive as an EVIDENCE
// material cross-linked with the exploded materials, all in one atomic
// commit — nothing is persisted unless the whole set succeeds.
mts, err := crafter.AddMaterialsFromArchive(ctx, attestationID, materialType, materialName, materialValue, format, casBackend, annotations, limits, withSourceArchiveEvidence(addOpts)...)
if err != nil {
return nil, fmt.Errorf("adding materials from archive: %w", err)
}

results := make([]*AttestationStatusMaterial, 0, len(mts))
for _, mt := range mts {
r, err := attMaterialToAction(mt)
Expand Down Expand Up @@ -248,6 +255,13 @@ func runtimeInputAddOpts(runtimeInputs *policies.RuntimeInputs) []crafter.AddOpt
return []crafter.AddOpt{crafter.WithRuntimeInputs(runtimeInputs)}
}

// withSourceArchiveEvidence extends opts so an archive explode also records the
// source archive as evidence. Defined at package scope so it can reference the
// crafter package, which the `crafter` local in Run() shadows.
func withSourceArchiveEvidence(opts []crafter.AddOpt) []crafter.AddOpt {
return append(opts, crafter.WithSourceArchiveEvidence())
}

// buildRuntimeInputs reads each policy input file and returns the extracted
// values grouped for the policy engine: unscoped entries under Global and
// policy-scoped entries under Scoped[policy]. Values are newline-joined and
Expand Down Expand Up @@ -323,24 +337,7 @@ func addReference(m *api.Attestation_Material, names ...string) {
if m.Annotations == nil {
m.Annotations = make(map[string]string)
}

existing := []string{}
if v := m.Annotations[materials.AnnotationMaterialReferences]; v != "" {
existing = strings.Split(v, ",")
}

seen := make(map[string]struct{}, len(existing))
for _, e := range existing {
seen[e] = struct{}{}
}
for _, n := range names {
if _, ok := seen[n]; !ok {
existing = append(existing, n)
seen[n] = struct{}{}
}
}

m.Annotations[materials.AnnotationMaterialReferences] = strings.Join(existing, ",")
m.Annotations[materials.AnnotationMaterialReferences] = materials.AppendReferences(m.Annotations[materials.AnnotationMaterialReferences], names...)
}

// policyInputEvidenceNames derives the evidence material name for each policy
Expand Down
12 changes: 1 addition & 11 deletions app/cli/pkg/action/attestation_add_routing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package action

import (
"archive/zip"
"os"
"path/filepath"
"testing"
Expand All @@ -31,16 +30,7 @@ import (
func writeTestZip(t *testing.T, dir, name string) string {
t.Helper()
path := filepath.Join(dir, name)
f, err := os.Create(path)
require.NoError(t, err)
defer f.Close()

w := zip.NewWriter(f)
entry, err := w.Create("entry.txt")
require.NoError(t, err)
_, err = entry.Write([]byte("hello"))
require.NoError(t, err)
require.NoError(t, w.Close())
writeZipWithFiles(t, path, map[string]string{"entry.txt": "hello"})
return path
}

Expand Down
73 changes: 73 additions & 0 deletions app/cli/pkg/action/attestation_add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@
package action

import (
"archive/zip"
"context"
"os"
"path/filepath"
"regexp"
"strings"
"testing"

schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials"
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners"
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/statemanager/filesystem"
"github.com/chainloop-dev/chainloop/pkg/casclient"
"github.com/chainloop-dev/chainloop/pkg/policies"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -32,6 +40,71 @@ import (
// names by the proto validation (name.dns-1123).
var materialNameRe = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)

// TestAddSourceArchiveEvidence exercises the Part B cross-link end to end: an
// exploded archive is recorded once as an EVIDENCE material and linked with the
// exploded materials in both directions.
// TestExplodeRecordsSourceArchiveEvidence checks that AddMaterialsFromArchive
// records the source archive once as an EVIDENCE material cross-linked with the
// exploded materials in both directions, all in the one atomic add.
func TestExplodeRecordsSourceArchiveEvidence(t *testing.T) {
ctx := context.Background()

// A dry-run crafter backed by a local state file (no control plane).
statePath := filepath.Join(t.TempDir(), "attestation.json")
sm, err := filesystem.New(statePath)
require.NoError(t, err)
c, err := crafter.NewCrafter(sm, nil)
require.NoError(t, err)
require.NoError(t, c.Init(ctx, &crafter.InitOpts{
SchemaV1: &schemaapi.CraftingSchema{SchemaVersion: "v1"},
WfInfo: &api.WorkflowMetadata{},
DryRun: true,
AttestationID: "",
Runner: runners.NewGeneric(),
}))

// A zip of two files exploded into "scan" / "scan-1".
zipPath := filepath.Join(t.TempDir(), "bundle.zip")
writeZipWithFiles(t, zipPath, map[string]string{"a.txt": "a", "b.txt": "b"})

backend := &casclient.CASBackend{}
mts, err := c.AddMaterialsFromArchive(ctx, "", "ARTIFACT", "scan", zipPath, materials.ArchiveZip, backend, nil, materials.DefaultArchiveLimits(), crafter.WithSourceArchiveEvidence())
require.NoError(t, err)
require.Len(t, mts, 2)

state := c.CraftingState.GetAttestation().GetMaterials()

// The archive is recorded once as EVIDENCE under "scan-archive".
ev, ok := state["scan-archive"]
require.True(t, ok, "expected scan-archive evidence material")
assert.Equal(t, schemaapi.CraftingSchema_Material_EVIDENCE, ev.GetMaterialType())

// Forward edge: the archive references exactly the exploded materials.
fwd := ev.GetAnnotations()[materials.AnnotationMaterialReferences]
assert.ElementsMatch(t, []string{"scan", "scan-1"}, strings.Split(fwd, ","))

// Reverse edge: every exploded material references the archive.
for _, name := range []string{"scan", "scan-1"} {
assert.Contains(t, state[name].GetAnnotations()[materials.AnnotationMaterialReferences], "scan-archive",
"exploded material %q must reference the archive", name)
}
}

func writeZipWithFiles(t *testing.T, path string, files map[string]string) {
t.Helper()
f, err := os.Create(path)
require.NoError(t, err)
defer f.Close()
zw := zip.NewWriter(f)
for name, content := range files {
w, err := zw.Create(name)
require.NoError(t, err)
_, err = w.Write([]byte(content))
require.NoError(t, err)
}
require.NoError(t, zw.Close())
}

func TestPolicyInputEvidenceNames(t *testing.T) {
testCases := []struct {
name string
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading