Skip to content

Commit 292d7be

Browse files
committed
feat(pr-detection): Autodetect PR information
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
1 parent f92556a commit 292d7be

33 files changed

Lines changed: 1427 additions & 292 deletions

app/cli/cmd/attestation_init.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/chainloop-dev/chainloop/app/cli/cmd/output"
2323
"github.com/chainloop-dev/chainloop/app/cli/pkg/action"
2424
"github.com/spf13/cobra"
25+
"github.com/spf13/viper"
2526
)
2627

2728
func newAttestationInitCmd() *cobra.Command {
@@ -77,11 +78,14 @@ func newAttestationInitCmd() *cobra.Command {
7778
RunE: func(cmd *cobra.Command, _ []string) error {
7879
a, err := action.NewAttestationInit(
7980
&action.AttestationInitOpts{
80-
ActionsOpts: ActionOpts,
81-
DryRun: attestationDryRun,
82-
Force: force,
83-
UseRemoteState: useAttestationRemoteState,
84-
LocalStatePath: attestationLocalStatePath,
81+
ActionsOpts: ActionOpts,
82+
DryRun: attestationDryRun,
83+
Force: force,
84+
UseRemoteState: useAttestationRemoteState,
85+
LocalStatePath: attestationLocalStatePath,
86+
CASURI: viper.GetString(confOptions.CASAPI.viperKey),
87+
CASCAPath: viper.GetString(confOptions.CASCA.viperKey),
88+
ConnectionInsecure: apiInsecure(),
8589
},
8690
)
8791
if err != nil {

app/cli/documentation/cli-reference.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ Options
200200
--annotation strings additional annotation in the format of key=value
201201
--attestation-id string Unique identifier of the in-progress attestation
202202
-h, --help help for add
203-
--kind string kind of the material to be recorded: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "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" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
203+
--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" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
204204
--name string name of the material as shown in the contract
205205
--registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD)
206206
--registry-server string OCI repository server, ($CHAINLOOP_REGISTRY_SERVER)
@@ -2871,7 +2871,7 @@ Options
28712871
--annotation strings Key-value pairs of material annotations (key=value)
28722872
-h, --help help for eval
28732873
--input stringArray Key-value pairs of policy inputs (key=value)
2874-
--kind string Kind of the material: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "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" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
2874+
--kind string Kind of the material: ["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" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
28752875
--material string Path to material or attestation file
28762876
-p, --policy string Policy reference (./my-policy.yaml, https://my-domain.com/my-policy.yaml, chainloop://my-stored-policy) (default "policy.yaml")
28772877
```

app/cli/pkg/action/attestation_init.go

Lines changed: 127 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@ package action
1717

1818
import (
1919
"context"
20+
"encoding/json"
2021
"errors"
2122
"fmt"
23+
"os"
2224
"strconv"
2325

2426
"github.com/chainloop-dev/chainloop/app/cli/internal/token"
2527
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2628
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2729
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal"
30+
"github.com/chainloop-dev/chainloop/internal/prinfo"
2831
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
2932
clientAPI "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
33+
"github.com/chainloop-dev/chainloop/pkg/casclient"
34+
"github.com/chainloop-dev/chainloop/pkg/grpcconn"
3035
"github.com/chainloop-dev/chainloop/pkg/policies"
3136
"github.com/rs/zerolog"
3237
)
@@ -37,16 +42,22 @@ type AttestationInitOpts struct {
3742
// Force the initialization and override any existing, in-progress ones.
3843
// Note that this is only useful when local-based attestation state is configured
3944
// since it's a protection to make sure you don't override the state by mistake
40-
Force bool
41-
UseRemoteState bool
42-
LocalStatePath string
45+
Force bool
46+
UseRemoteState bool
47+
LocalStatePath string
48+
CASURI string
49+
CASCAPath string // optional CA certificate for the CAS connection
50+
ConnectionInsecure bool
4351
}
4452

4553
type AttestationInit struct {
4654
*ActionsOpts
47-
dryRun, force bool
48-
c *crafter.Crafter
49-
useRemoteState bool
55+
dryRun, force bool
56+
c *crafter.Crafter
57+
useRemoteState bool
58+
casURI string
59+
casCAPath string
60+
connectionInsecure bool
5061
}
5162

5263
// ErrAttestationAlreadyExist means that there is an attestation in progress
@@ -67,11 +78,14 @@ func NewAttestationInit(cfg *AttestationInitOpts) (*AttestationInit, error) {
6778
}
6879

6980
return &AttestationInit{
70-
ActionsOpts: cfg.ActionsOpts,
71-
c: c,
72-
dryRun: cfg.DryRun,
73-
force: cfg.Force,
74-
useRemoteState: cfg.UseRemoteState,
81+
ActionsOpts: cfg.ActionsOpts,
82+
c: c,
83+
dryRun: cfg.DryRun,
84+
force: cfg.Force,
85+
useRemoteState: cfg.UseRemoteState,
86+
casURI: cfg.CASURI,
87+
casCAPath: cfg.CASCAPath,
88+
connectionInsecure: cfg.ConnectionInsecure,
7589
}, nil
7690
}
7791

@@ -219,6 +233,44 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
219233
attestationID = workflowRun.GetId()
220234
}
221235

236+
// Get CAS credentials for PR metadata upload
237+
var casBackend = &casclient.CASBackend{Name: "not-set"}
238+
if !action.dryRun && attestationID != "" {
239+
creds, err := client.GetUploadCreds(ctx,
240+
&pb.AttestationServiceGetUploadCredsRequest{
241+
WorkflowRunId: attestationID,
242+
},
243+
)
244+
if err != nil {
245+
// Log warning but don't fail - will fall back to inline storage
246+
action.Logger.Warn().Err(err).Msg("failed to get CAS credentials for PR metadata, will store inline")
247+
} else {
248+
b := creds.GetResult().GetBackend()
249+
if b != nil {
250+
casBackend.Name = b.Provider
251+
casBackend.MaxSize = b.GetLimits().MaxBytes
252+
253+
// Set up CAS connection if not inline
254+
if !b.IsInline && creds.Result.Token != "" {
255+
var opts = []grpcconn.Option{
256+
grpcconn.WithInsecure(action.connectionInsecure),
257+
}
258+
if action.casCAPath != "" {
259+
opts = append(opts, grpcconn.WithCAFile(action.casCAPath))
260+
}
261+
262+
artifactCASConn, err := grpcconn.New(action.casURI, creds.Result.Token, opts...)
263+
if err != nil {
264+
action.Logger.Warn().Err(err).Msg("failed to create CAS connection, will store inline")
265+
} else {
266+
defer artifactCASConn.Close()
267+
casBackend.Uploader = casclient.New(artifactCASConn, casclient.WithLogger(action.Logger))
268+
}
269+
}
270+
}
271+
}
272+
}
273+
222274
var authInfo *clientAPI.Attestation_Auth
223275
if action.AuthTokenRaw != "" {
224276
authInfo, err = extractAuthInfo(action.AuthTokenRaw)
@@ -268,6 +320,12 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
268320
return "", err
269321
}
270322

323+
// Auto-collect PR/MR metadata if in PR/MR context
324+
if err := action.autoCollectPRMetadata(ctx, attestationID, discoveredRunner, casBackend); err != nil {
325+
action.Logger.Warn().Err(err).Msg("failed to auto-collect PR/MR metadata")
326+
// Don't fail the init - this is best-effort
327+
}
328+
271329
return attestationID, nil
272330
}
273331

@@ -371,6 +429,64 @@ func extractAuthInfo(authToken string) (*clientAPI.Attestation_Auth, error) {
371429
}, nil
372430
}
373431

432+
// autoCollectPRMetadata automatically collects PR/MR metadata if running in a PR/MR context
433+
func (action *AttestationInit) autoCollectPRMetadata(ctx context.Context, attestationID string, runner crafter.SupportedRunner, casBackend *casclient.CASBackend) error {
434+
// Detect if we're in a PR/MR context
435+
isPR, metadata, err := crafter.DetectPRContext(runner)
436+
if err != nil {
437+
return fmt.Errorf("failed to detect PR/MR context: %w", err)
438+
}
439+
440+
// If not in PR/MR context, nothing to do
441+
if !isPR {
442+
action.Logger.Debug().Msg("not in PR/MR context, skipping metadata collection")
443+
return nil
444+
}
445+
446+
action.Logger.Info().Str("platform", metadata.Platform).Str("number", metadata.Number).Msg("detected PR/MR context")
447+
448+
// Create the material
449+
evidenceData := prinfo.NewEvidence(prinfo.Data{
450+
Platform: metadata.Platform,
451+
Type: metadata.Type,
452+
Number: metadata.Number,
453+
Title: metadata.Title,
454+
Description: metadata.Description,
455+
SourceBranch: metadata.SourceBranch,
456+
TargetBranch: metadata.TargetBranch,
457+
URL: metadata.URL,
458+
Author: metadata.Author,
459+
})
460+
461+
// Marshal to JSON
462+
jsonData, err := json.Marshal(evidenceData)
463+
if err != nil {
464+
return fmt.Errorf("failed to marshal PR/MR metadata: %w", err)
465+
}
466+
467+
// Create a temporary file for the metadata
468+
tmpFile, err := os.CreateTemp("", "pr-metadata-*.json")
469+
if err != nil {
470+
return fmt.Errorf("failed to create temp file: %w", err)
471+
}
472+
defer os.Remove(tmpFile.Name())
473+
474+
// Write the JSON data to the temp file
475+
if _, err := tmpFile.Write(jsonData); err != nil {
476+
tmpFile.Close()
477+
return fmt.Errorf("failed to write metadata to temp file: %w", err)
478+
}
479+
tmpFile.Close()
480+
481+
// Add the material using the crafter with explicit CHAINLOOP_PR_INFO type
482+
if _, err := action.c.AddMaterialContractFree(ctx, attestationID, v1.CraftingSchema_Material_CHAINLOOP_PR_INFO.String(), "pr-metadata", tmpFile.Name(), casBackend, nil); err != nil {
483+
return fmt.Errorf("failed to add PR/MR metadata material: %w", err)
484+
}
485+
486+
action.Logger.Info().Msg("successfully collected and attested PR/MR metadata")
487+
return nil
488+
}
489+
374490
// parseContractV2 attempts to parse a raw contract as V2 schema
375491
func parseContractV2(rawContract *pb.WorkflowContractVersionItem_RawBody) *v1.CraftingSchemaV2 {
376492
if rawContract == nil {

app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)