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
3 changes: 3 additions & 0 deletions app/cli/cmd/attestation_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func newAttestationAddCmd() *cobra.Command {
var name, value, kind string
var artifactCASConn *grpc.ClientConn
var annotationsFlag []string
var noStrictValidation bool

// OCI registry credentials can be passed as flags or environment variables
var registryServer, registryUsername, registryPassword string
Expand Down Expand Up @@ -76,6 +77,7 @@ func newAttestationAddCmd() *cobra.Command {
RegistryUsername: registryUsername,
RegistryPassword: registryPassword,
LocalStatePath: attestationLocalStatePath,
NoStrictValidation: noStrictValidation,
},
)
if err != nil {
Expand Down Expand Up @@ -142,6 +144,7 @@ func newAttestationAddCmd() *cobra.Command {
cmd.Flags().StringSliceVar(&annotationsFlag, "annotation", nil, "additional annotation in the format of key=value")
flagAttestationID(cmd)
cmd.Flags().StringVar(&kind, "kind", "", fmt.Sprintf("kind of the material to be recorded: %q", schemaapi.ListAvailableMaterialKind()))
cmd.Flags().BoolVar(&noStrictValidation, "no-strict-validation", false, "skip strict schema validation for SBOM_CYCLONEDX_JSON materials")

// Optional OCI registry credentials
cmd.Flags().StringVar(&registryServer, "registry-server", "", fmt.Sprintf("OCI repository server, ($%s)", registryServerEnvVarName))
Expand Down
1 change: 1 addition & 0 deletions app/cli/documentation/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ Options
-h, --help help for add
--kind string kind of the material to be recorded: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
--name string name of the material as shown in the contract
--no-strict-validation skip strict schema validation for SBOM_CYCLONEDX_JSON materials
--registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD)
--registry-server string OCI repository server, ($CHAINLOOP_REGISTRY_SERVER)
--registry-username string registry username, ($CHAINLOOP_REGISTRY_USERNAME)
Expand Down
2 changes: 1 addition & 1 deletion app/cli/internal/policydevel/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func craft(materialPath string, kind v1.CraftingSchema_Material_MaterialType, na
Name: name,
}

m, err := materials.Craft(context.Background(), materialSchema, materialPath, backend, nil, logger)
m, err := materials.Craft(context.Background(), materialSchema, materialPath, backend, nil, logger, nil)
if err != nil {
return nil, fmt.Errorf("failed to craft material (kind=%s): %w", kind.String(), err)
}
Expand Down
5 changes: 5 additions & 0 deletions app/cli/pkg/action/attestation_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type AttestationAddOpts struct {
// OCI registry credentials used for CONTAINER_IMAGE material type
RegistryServer, RegistryUsername, RegistryPassword string
LocalStatePath string
// NoStrictValidation skips strict schema validation
NoStrictValidation bool
}

type newCrafterOpts struct {
Expand All @@ -59,6 +61,9 @@ func NewAttestationAdd(cfg *AttestationAddOpts) (*AttestationAdd, error) {
cfg.Logger.Debug().Str("server", cfg.RegistryServer).Str("username", cfg.RegistryUsername).Msg("using OCI registry credentials")
opts = append(opts, crafter.WithOCIAuth(cfg.RegistryServer, cfg.RegistryUsername, cfg.RegistryPassword))
}
if cfg.NoStrictValidation {
opts = append(opts, crafter.WithNoStrictValidation(cfg.NoStrictValidation))
}

return &AttestationAdd{
ActionsOpts: cfg.ActionsOpts,
Expand Down
14 changes: 13 additions & 1 deletion pkg/attestation/crafter/crafter.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ type Crafter struct {

// attestation client is used to load chainloop policies
attClient v1.AttestationServiceClient

// noStrictValidation skips strict schema validation
noStrictValidation bool
}

type VersionedCraftingState struct {
Expand Down Expand Up @@ -118,6 +121,13 @@ func WithOCIAuth(server, username, password string) NewOpt {
}
}

func WithNoStrictValidation(noStrictValidation bool) NewOpt {
return func(c *Crafter) error {
c.noStrictValidation = noStrictValidation
return nil
}
}

// Create a completely new crafter
func NewCrafter(stateManager StateManager, attClient v1.AttestationServiceClient, opts ...NewOpt) (*Crafter, error) {
noopLogger := zerolog.Nop()
Expand Down Expand Up @@ -671,7 +681,9 @@ func (c *Crafter) AddMaterialContactFreeWithAutoDetectedKind(ctx context.Context
// addMaterials adds the incoming material m to the crafting state
func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_Material, attestationID, value string, casBackend *casclient.CASBackend, runtimeAnnotations map[string]string) (*api.Attestation_Material, error) {
// 3- Craft resulting material
mt, err := materials.Craft(context.Background(), m, value, casBackend, c.ociRegistryAuth, c.Logger)
mt, err := materials.Craft(context.Background(), m, value, casBackend, c.ociRegistryAuth, c.Logger, &materials.CraftingOpts{
NoStrictValidation: c.noStrictValidation,
})
if err != nil {
return nil, err
}
Expand Down
55 changes: 47 additions & 8 deletions pkg/attestation/crafter/materials/cyclonedxjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,28 @@ const (
)

type CyclonedxJSONCrafter struct {
backend *casclient.CASBackend
backend *casclient.CASBackend
noStrictValidation bool
*crafterCommon
}

// CycloneDXCraftOpt is a functional option for CyclonedxJSONCrafter
type CycloneDXCraftOpt func(*CyclonedxJSONCrafter)

// WithCycloneDXNoStrictValidation sets the noStrictValidation option
func WithCycloneDXNoStrictValidation(noStrict bool) CycloneDXCraftOpt {
return func(c *CyclonedxJSONCrafter) {
c.noStrictValidation = noStrict
}
}

// cyclonedxRequiredFields checks the three required top-level fields per CycloneDX spec
type cyclonedxRequiredFields struct {
BOMFormat string `json:"bomFormat"`
SpecVersion string `json:"specVersion"`
Version int `json:"version"`
}

// cyclonedxDoc internal struct to unmarshall the incoming CycloneDX JSON
type cyclonedxDoc struct {
SpecVersion string `json:"specVersion"`
Expand Down Expand Up @@ -78,15 +96,21 @@ type cyclonedxMetadataV15 struct {
Component cyclonedxComponent `json:"component"`
}

func NewCyclonedxJSONCrafter(materialSchema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*CyclonedxJSONCrafter, error) {
func NewCyclonedxJSONCrafter(materialSchema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger, opts ...CycloneDXCraftOpt) (*CyclonedxJSONCrafter, error) {
if materialSchema.Type != schemaapi.CraftingSchema_Material_SBOM_CYCLONEDX_JSON {
return nil, fmt.Errorf("material type is not cyclonedx json")
}

return &CyclonedxJSONCrafter{
c := &CyclonedxJSONCrafter{
backend: backend,
crafterCommon: &crafterCommon{logger: l, input: materialSchema},
}, nil
}

for _, opt := range opts {
opt(c)
}

return c, nil
}

func (i *CyclonedxJSONCrafter) Craft(ctx context.Context, filePath string) (*api.Attestation_Material, error) {
Expand All @@ -95,17 +119,32 @@ func (i *CyclonedxJSONCrafter) Craft(ctx context.Context, filePath string) (*api
return nil, fmt.Errorf("can't open the file: %w", err)
}

var required cyclonedxRequiredFields
if err := json.Unmarshal(f, &required); err != nil {
i.logger.Debug().Err(err).Msg("error decoding file")
return nil, fmt.Errorf("invalid cyclonedx sbom file: %w", ErrInvalidMaterialType)
}

if required.BOMFormat != "CycloneDX" || required.SpecVersion == "" || required.Version < 1 {
i.logger.Debug().Str("bomFormat", required.BOMFormat).Str("specVersion", required.SpecVersion).Int("version", required.Version).Msg("missing required CycloneDX fields")
return nil, fmt.Errorf("invalid cyclonedx sbom file: %w", ErrInvalidMaterialType)
}

var v interface{}
if err := json.Unmarshal(f, &v); err != nil {
i.logger.Debug().Err(err).Msg("error decoding file")
return nil, fmt.Errorf("invalid cyclonedx sbom file: %w", ErrInvalidMaterialType)
}

// Setting the version to empty string to validate against the latest version of the schema
err = schemavalidators.ValidateCycloneDX(v, "")
if err != nil {
i.logger.Debug().Err(err).Msgf("error decoding file: %#v", err)
return nil, fmt.Errorf("invalid cyclonedx sbom file: %w", ErrInvalidMaterialType)
if err := schemavalidators.ValidateCycloneDX(v, ""); err != nil {
if i.noStrictValidation {
i.logger.Warn().Err(err).Msg("error decoding file, strict validation disabled, continuing")
} else {
i.logger.Debug().Err(err).Msg("error decoding file")
i.logger.Info().Msg("you can disable strict validation to skip schema validation")
return nil, fmt.Errorf("invalid cyclonedx sbom file: %w", ErrInvalidMaterialType)
}
}

m, err := uploadAndCraft(ctx, i.input, i.backend, filePath, i.logger)
Expand Down
66 changes: 66 additions & 0 deletions pkg/attestation/crafter/materials/cyclonedxjson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,72 @@ func TestCyclonedxJSONCraft(t *testing.T) {
}
}

func TestCycloneDXJSONCraftNoStrictValidation(t *testing.T) {
testCases := []struct {
name string
filePath string
noStrictValidation bool
wantErr string
}{
{
name: "invalid schema without skip flag fails",
filePath: "./testdata/sbom.cyclonedx-invalid-schema.json",
noStrictValidation: false,
wantErr: "invalid cyclonedx sbom file",
},
{
name: "invalid schema with skip flag succeeds",
filePath: "./testdata/sbom.cyclonedx-invalid-schema.json",
noStrictValidation: true,
wantErr: "",
},
{
name: "non-cyclonedx file fails even with skip flag",
filePath: "./testdata/random.json",
noStrictValidation: true,
wantErr: "invalid cyclonedx sbom file",
},
{
name: "valid file works without skip flag",
filePath: "./testdata/sbom.cyclonedx.json",
noStrictValidation: false,
wantErr: "",
},
}

schema := &contractAPI.CraftingSchema_Material{
Name: "test",
Type: contractAPI.CraftingSchema_Material_SBOM_CYCLONEDX_JSON,
}
l := zerolog.Nop()

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ast := assert.New(t)
// Mock uploader
uploader := mUploader.NewUploader(t)
if tc.wantErr == "" {
uploader.On("UploadFile", context.TODO(), tc.filePath).
Return(&casclient.UpDownStatus{}, nil)
}

backend := &casclient.CASBackend{Uploader: uploader}
crafter, err := materials.NewCyclonedxJSONCrafter(schema, backend, &l,
materials.WithCycloneDXNoStrictValidation(tc.noStrictValidation))
require.NoError(t, err)

got, err := crafter.Craft(context.TODO(), tc.filePath)
if tc.wantErr != "" {
ast.ErrorContains(err, tc.wantErr)
return
}

require.NoError(t, err)
ast.Equal(contractAPI.CraftingSchema_Material_SBOM_CYCLONEDX_JSON.String(), got.MaterialType.String())
})
}
}

func TestCycloneDXJSONCraft_SkipUpload(t *testing.T) {
testCases := []struct {
name string
Expand Down
14 changes: 12 additions & 2 deletions pkg/attestation/crafter/materials/materials.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,13 @@ type Craftable interface {
Craft(ctx context.Context, value string) (*api.Attestation_Material, error)
}

func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Material, value string, casBackend *casclient.CASBackend, ociAuth authn.Keychain, logger *zerolog.Logger) (*api.Attestation_Material, error) {
// CraftingOpts contains options for crafting materials
type CraftingOpts struct {
NoStrictValidation bool
}

//nolint:gocyclo
func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Material, value string, casBackend *casclient.CASBackend, ociAuth authn.Keychain, logger *zerolog.Logger, opts *CraftingOpts) (*api.Attestation_Material, error) {
var crafter Craftable
var err error

Expand All @@ -218,6 +224,10 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia
return nil, fmt.Errorf("validating material: %w", err)
}

if opts == nil {
opts = &CraftingOpts{}
}

switch materialSchema.Type {
case schemaapi.CraftingSchema_Material_STRING:
crafter, err = NewStringCrafter(materialSchema)
Expand All @@ -226,7 +236,7 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia
case schemaapi.CraftingSchema_Material_ARTIFACT:
crafter, err = NewArtifactCrafter(materialSchema, casBackend, logger)
case schemaapi.CraftingSchema_Material_SBOM_CYCLONEDX_JSON:
crafter, err = NewCyclonedxJSONCrafter(materialSchema, casBackend, logger)
crafter, err = NewCyclonedxJSONCrafter(materialSchema, casBackend, logger, WithCycloneDXNoStrictValidation(opts.NoStrictValidation))
case schemaapi.CraftingSchema_Material_SBOM_SPDX_JSON:
crafter, err = NewSPDXJSONCrafter(materialSchema, casBackend, logger)
case schemaapi.CraftingSchema_Material_JUNIT_XML:
Expand Down
2 changes: 1 addition & 1 deletion pkg/attestation/crafter/materials/materials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestCraft(t *testing.T) {
},
}

got, err := materials.Craft(context.TODO(), schema, "test-value", nil, nil, nil)
got, err := materials.Craft(context.TODO(), schema, "test-value", nil, nil, nil, nil)
require.NoError(t, err)
assert.Equal(contractAPI.CraftingSchema_Material_STRING, got.MaterialType)
assert.False(got.UploadedToCas)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"version": 1,
"invalidField": "this field is not valid according to the schema",
"components": "should be an array but is a string"
}
Loading