Skip to content

Commit 76a41fd

Browse files
authored
feat(attestation): Allow the auto discovery of material's kind (#820)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent b8ccc49 commit 76a41fd

File tree

5 files changed

+147
-15
lines changed

5 files changed

+147
-15
lines changed

app/cli/cmd/attestation_add.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ func newAttestationAddCmd() *cobra.Command {
5252
chainloop attestation add --name <material-name> --value <material-value>
5353
5454
# Add a material to the attestation that is not defined in the contract but you know the kind
55-
chainloop attestation add --kind <material-kind> --value <material-value>`,
55+
chainloop attestation add --kind <material-kind> --value <material-value>
56+
57+
# Add a material to the attestation without specifying neither kind nor name enables automatic detection
58+
chainloop attestation add --value <material-value>`,
5659
PreRunE: func(cmd *cobra.Command, args []string) error {
57-
switch {
58-
case name == "" && kind == "":
59-
return fmt.Errorf("either --name or --kind needs to be set")
60-
case name != "" && kind != "":
60+
if name != "" && kind != "" {
6161
return fmt.Errorf("both --name and --kind cannot be set at the same time")
6262
}
6363

app/cli/internal/action/attestation_add.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222

2323
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
24+
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2425
"github.com/chainloop-dev/chainloop/internal/attestation/crafter"
2526
"github.com/chainloop-dev/chainloop/internal/casclient"
2627
"github.com/chainloop-dev/chainloop/internal/grpcconn"
@@ -110,16 +111,23 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa
110111

111112
casBackend.Uploader = casclient.New(artifactCASConn, casclient.WithLogger(action.Logger))
112113
}
113-
114-
// Add material to the attestation crafting state based on if the material is contract free or not
115-
if materialName != "" {
116-
if err := action.c.AddMaterialFromContract(ctx, attestationID, materialName, materialValue, casBackend, annotations); err != nil {
117-
return fmt.Errorf("adding material: %w", err)
118-
}
119-
} else {
120-
if err := action.c.AddMaterialContractFree(ctx, attestationID, materialType, materialValue, casBackend, annotations); err != nil {
114+
// Add material to the attestation crafting state based on if the material is contract free or not.
115+
// By default, try to detect the material kind automatically
116+
switch {
117+
case materialName == "" && materialType == "":
118+
var kind schemaapi.CraftingSchema_Material_MaterialType
119+
if kind, err = action.c.AddMaterialContactFreeAutomatic(ctx, attestationID, materialValue, casBackend, annotations); err != nil {
121120
return fmt.Errorf("adding material: %w", err)
122121
}
122+
action.Logger.Info().Str("kind", kind.String()).Msg("material kind detected")
123+
case materialName != "":
124+
err = action.c.AddMaterialFromContract(ctx, attestationID, materialName, materialValue, casBackend, annotations)
125+
default:
126+
err = action.c.AddMaterialContractFree(ctx, attestationID, materialType, materialValue, casBackend, annotations)
127+
}
128+
129+
if err != nil {
130+
return fmt.Errorf("adding material: %w", err)
123131
}
124132

125133
return nil

app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@ import (
2020
"strings"
2121
)
2222

23+
// CraftingMaterialInValidationOrder all type of CraftingMaterial that are available for automatic
24+
// detection. The order of the list is important as it defines the order of the
25+
// detection process. Normally from most common one to the least common one and weaker validation method.
26+
var CraftingMaterialInValidationOrder = []CraftingSchema_Material_MaterialType{
27+
CraftingSchema_Material_OPENVEX,
28+
CraftingSchema_Material_SBOM_CYCLONEDX_JSON,
29+
CraftingSchema_Material_SBOM_SPDX_JSON,
30+
CraftingSchema_Material_CSAF_VEX,
31+
CraftingSchema_Material_CSAF_INFORMATIONAL_ADVISORY,
32+
CraftingSchema_Material_CSAF_SECURITY_ADVISORY,
33+
CraftingSchema_Material_CSAF_SECURITY_INCIDENT_RESPONSE,
34+
CraftingSchema_Material_JUNIT_XML,
35+
CraftingSchema_Material_HELM_CHART,
36+
CraftingSchema_Material_CONTAINER_IMAGE,
37+
CraftingSchema_Material_SARIF,
38+
CraftingSchema_Material_ATTESTATION,
39+
CraftingSchema_Material_ARTIFACT,
40+
CraftingSchema_Material_STRING,
41+
}
42+
2343
// ListAvailableMaterialKind returns a list of available material kinds
2444
func ListAvailableMaterialKind() []string {
2545
var res []string

internal/attestation/crafter/crafter.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,31 @@ func (c *Crafter) AddMaterialFromContract(ctx context.Context, attestationID, ke
514514
return c.addMaterial(ctx, m, attestationID, value, casBackend, runtimeAnnotations)
515515
}
516516

517+
// AddMaterialContactFreeAutomatic adds a material to the crafting state checking the incoming material matches any of the
518+
// supported types in validation order. If the material is not found it will return an error.
519+
func (c *Crafter) AddMaterialContactFreeAutomatic(ctx context.Context, attestationID, value string, casBackend *casclient.CASBackend, runtimeAnnotations map[string]string) (schemaapi.CraftingSchema_Material_MaterialType, error) {
520+
var kind schemaapi.CraftingSchema_Material_MaterialType
521+
var found bool
522+
523+
// We want to run the material validation in a specific order
524+
for _, kind = range schemaapi.CraftingMaterialInValidationOrder {
525+
if err := c.AddMaterialContractFree(ctx, attestationID, kind.String(), value, casBackend, runtimeAnnotations); err != nil {
526+
c.logger.Debug().Err(err).Str("kind", kind.String()).Msg("failed to add material")
527+
continue
528+
}
529+
// If we found a match we break the loop and stop looking
530+
found = true
531+
break
532+
}
533+
534+
// Return an error if the material could not be added
535+
if !found {
536+
return kind, fmt.Errorf("failed to add material with attestationID: %s", attestationID)
537+
}
538+
539+
return kind, nil
540+
}
541+
517542
// addMaterials adds the incoming material m to the crafting state
518543
func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_Material, attestationID, value string, casBackend *casclient.CASBackend, runtimeAnnotations map[string]string) error {
519544
// 3- Craft resulting material

internal/attestation/crafter/crafter_test.go

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ import (
2828
v1 "github.com/chainloop-dev/chainloop/internal/attestation/crafter/api/attestation/v1"
2929
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/runners"
3030
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/statemanager/filesystem"
31+
"github.com/chainloop-dev/chainloop/internal/casclient"
32+
mUploader "github.com/chainloop-dev/chainloop/internal/casclient/mocks"
33+
3134
"github.com/go-git/go-git/v5"
3235
"github.com/go-git/go-git/v5/plumbing/object"
36+
"github.com/stretchr/testify/assert"
3337
"github.com/stretchr/testify/require"
34-
"google.golang.org/protobuf/proto"
35-
3638
"github.com/stretchr/testify/suite"
39+
"google.golang.org/protobuf/proto"
3740
)
3841

3942
type crafterSuite struct {
@@ -417,3 +420,79 @@ func (s *crafterSuite) SetupTest() {
417420
func TestSuite(t *testing.T) {
418421
suite.Run(t, new(crafterSuite))
419422
}
423+
424+
func (s *crafterSuite) TestAddMaterialsAutomatic() {
425+
testCases := []struct {
426+
name string
427+
materialPath string
428+
expectedType schemaapi.CraftingSchema_Material_MaterialType
429+
wantErr bool
430+
}{
431+
{
432+
name: "sarif",
433+
materialPath: "./materials/testdata/report.sarif",
434+
expectedType: schemaapi.CraftingSchema_Material_SARIF,
435+
},
436+
{
437+
name: "openvex",
438+
materialPath: "./materials/testdata/openvex_v0.2.0.json",
439+
expectedType: schemaapi.CraftingSchema_Material_OPENVEX,
440+
},
441+
{
442+
name: "HELM CHART",
443+
materialPath: "./materials/testdata/valid-chart.tgz",
444+
expectedType: schemaapi.CraftingSchema_Material_HELM_CHART,
445+
},
446+
{
447+
name: "junit",
448+
materialPath: "./materials/testdata/junit.xml",
449+
expectedType: schemaapi.CraftingSchema_Material_JUNIT_XML,
450+
},
451+
{
452+
name: "artifact",
453+
materialPath: "./materials/testdata/missing-empty.tgz",
454+
expectedType: schemaapi.CraftingSchema_Material_ARTIFACT,
455+
},
456+
{
457+
name: "artifact - invalid junit",
458+
materialPath: "./materials/testdata/junit-invalid.xml",
459+
expectedType: schemaapi.CraftingSchema_Material_ARTIFACT,
460+
},
461+
{
462+
name: "artifact - random file",
463+
materialPath: "./materials/testdata/random.json",
464+
expectedType: schemaapi.CraftingSchema_Material_ARTIFACT,
465+
},
466+
{
467+
name: "random string",
468+
materialPath: "random-string",
469+
expectedType: schemaapi.CraftingSchema_Material_STRING,
470+
wantErr: true,
471+
},
472+
}
473+
474+
for _, tc := range testCases {
475+
s.Run(tc.name, func() {
476+
var runner crafter.SupportedRunner = runners.NewGeneric()
477+
contract := "testdata/contracts/empty_generic.yaml"
478+
uploader := mUploader.NewUploader(s.T())
479+
480+
if !tc.wantErr {
481+
uploader.On("UploadFile", context.Background(), tc.materialPath).
482+
Return(&casclient.UpDownStatus{
483+
Digest: "deadbeef",
484+
Filename: "simple.txt",
485+
}, nil)
486+
}
487+
488+
backend := &casclient.CASBackend{Uploader: uploader}
489+
490+
c, err := newInitializedCrafter(s.T(), contract, &v1.WorkflowMetadata{}, false, "", runner)
491+
require.NoError(s.T(), err)
492+
493+
kind, err := c.AddMaterialContactFreeAutomatic(context.Background(), "random-id", tc.materialPath, backend, nil)
494+
require.NoError(s.T(), err)
495+
assert.Equal(s.T(), tc.expectedType.String(), kind.String())
496+
})
497+
}
498+
}

0 commit comments

Comments
 (0)