Skip to content

Commit

Permalink
Add support for provenance. (#54)
Browse files Browse the repository at this point in the history
This change adds support for `/v2/provenance` endpoint, plus a small
fix on the values of `ProvenanceType`.
  • Loading branch information
blkt authored Nov 21, 2024
1 parent fd7f9d6 commit 089f44e
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 53 deletions.
63 changes: 47 additions & 16 deletions cmd/example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,38 @@ import (
)

func main() {
var endpoint, pname string
var endpoint, pname, ptype string
flag.StringVar(&endpoint, "endpoint", "", "Trusty API endpoint to call")
flag.StringVar(&pname, "pname", "", "Package name")
flag.StringVar(&ptype, "ptype", "", "Package type")
flag.Parse()

ctx := context.Background()
client := v2client.New()

input := &v2types.Dependency{
PackageName: pname,
PackageType: ptype,
}

switch endpoint {
case "summary":
if err := summary(ctx, client, pname); err != nil {
if err := summary(ctx, client, input); err != nil {
fmt.Fprintf(os.Stderr, "error calling endpoint: %s\n", err)
os.Exit(1)
}
case "pkg-meta":
if err := pkg(ctx, client, pname); err != nil {
if err := pkg(ctx, client, input); err != nil {
fmt.Fprintf(os.Stderr, "error calling endpoint: %s\n", err)
os.Exit(1)
}
case "alternatives":
if err := alternatives(ctx, client, pname); err != nil {
if err := alternatives(ctx, client, input); err != nil {
fmt.Fprintf(os.Stderr, "error calling endpoint: %s\n", err)
os.Exit(1)
}
case "provenance":
if err := provenance(ctx, client, input); err != nil {
fmt.Fprintf(os.Stderr, "error calling endpoint: %s\n", err)
os.Exit(1)
}
Expand All @@ -59,10 +70,12 @@ func main() {
}
}

func summary(ctx context.Context, client v2client.Trusty, pname string) error {
res, err := client.Summary(ctx, &v2types.Dependency{
PackageName: pname,
})
func summary(
ctx context.Context,
client v2client.Trusty,
input *v2types.Dependency,
) error {
res, err := client.Summary(ctx, input)
if err != nil {
return err
}
Expand All @@ -71,10 +84,12 @@ func summary(ctx context.Context, client v2client.Trusty, pname string) error {
return nil
}

func pkg(ctx context.Context, client v2client.Trusty, pname string) error {
res, err := client.PackageMetadata(ctx, &v2types.Dependency{
PackageName: pname,
})
func pkg(
ctx context.Context,
client v2client.Trusty,
input *v2types.Dependency,
) error {
res, err := client.PackageMetadata(ctx, input)
if err != nil {
return err
}
Expand All @@ -88,10 +103,26 @@ func pkg(ctx context.Context, client v2client.Trusty, pname string) error {
return nil
}

func alternatives(ctx context.Context, client v2client.Trusty, pname string) error {
res, err := client.Alternatives(ctx, &v2types.Dependency{
PackageName: pname,
})
func alternatives(
ctx context.Context,
client v2client.Trusty,
input *v2types.Dependency,
) error {
res, err := client.Alternatives(ctx, input)
if err != nil {
return err
}

fmt.Printf("%+v\n", res)
return nil
}

func provenance(
ctx context.Context,
client v2client.Trusty,
input *v2types.Dependency,
) error {
res, err := client.Provenance(ctx, input)
if err != nil {
return err
}
Expand Down
53 changes: 44 additions & 9 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ const (
v2SummaryPath = "v2/summary"
v2PkgPath = "v2/pkg"
v2Alternatives = "v2/alternatives"
v2Provenance = "v2/provenance"
)

// Summary fetches a summary of Security Signal information
Expand All @@ -336,6 +337,9 @@ func (t *Trusty) Summary(
if dep.PackageName == "" {
return nil, fmt.Errorf("dependency has no name defined")
}
if dep.PackageType == "" {
return nil, fmt.Errorf("dependency has no ecosystem defined")
}

u, err := urlFor(t.Options.BaseURL, v2SummaryPath)
if err != nil {
Expand All @@ -346,9 +350,7 @@ func (t *Trusty) Summary(
// package_version.
q := u.Query()
q.Set("package_name", dep.PackageName)
if dep.PackageType != nil && *dep.PackageType != "" {
q.Set("package_type", strings.ToLower(*dep.PackageType))
}
q.Set("package_type", strings.ToLower(dep.PackageType))
if dep.PackageVersion != nil && *dep.PackageVersion != "" {
q.Set("package_version", *dep.PackageVersion)
}
Expand All @@ -368,6 +370,9 @@ func (t *Trusty) PackageMetadata(
if dep.PackageName == "" {
return nil, fmt.Errorf("dependency has no name defined")
}
if dep.PackageType == "" {
return nil, fmt.Errorf("dependency has no ecosystem defined")
}

u, err := urlFor(t.Options.BaseURL, v2PkgPath)
if err != nil {
Expand All @@ -378,9 +383,7 @@ func (t *Trusty) PackageMetadata(
// package_version.
q := u.Query()
q.Set("package_name", dep.PackageName)
if dep.PackageType != nil && *dep.PackageType != "" {
q.Set("package_type", strings.ToLower(*dep.PackageType))
}
q.Set("package_type", strings.ToLower(dep.PackageType))
if dep.PackageVersion != nil && *dep.PackageVersion != "" {
q.Set("package_version", *dep.PackageVersion)
}
Expand All @@ -398,6 +401,9 @@ func (t *Trusty) Alternatives(
if dep.PackageName == "" {
return nil, fmt.Errorf("dependency has no name defined")
}
if dep.PackageType == "" {
return nil, fmt.Errorf("dependency has no ecosystem defined")
}

u, err := urlFor(t.Options.BaseURL, v2Alternatives)
if err != nil {
Expand All @@ -408,9 +414,7 @@ func (t *Trusty) Alternatives(
// package_version.
q := u.Query()
q.Set("package_name", dep.PackageName)
if dep.PackageType != nil && *dep.PackageType != "" {
q.Set("package_type", strings.ToLower(*dep.PackageType))
}
q.Set("package_type", strings.ToLower(dep.PackageType))
if dep.PackageVersion != nil && *dep.PackageVersion != "" {
q.Set("package_version", *dep.PackageVersion)
}
Expand All @@ -419,6 +423,37 @@ func (t *Trusty) Alternatives(
return doRequest[v2types.PackageAlternatives](t.Options.HttpClient, u.String())
}

// Provenance fetches detailed provenance information of a given
// package.
func (t *Trusty) Provenance(
_ context.Context,
dep *v2types.Dependency,
) (*v2types.Provenance, error) {
if dep.PackageName == "" {
return nil, fmt.Errorf("dependency has no name defined")
}
if dep.PackageType == "" {
return nil, fmt.Errorf("dependency has no ecosystem defined")
}

u, err := urlFor(t.Options.BaseURL, v2Provenance)
if err != nil {
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
}

// Add query parameters for package_name, package_type, and
// package_version.
q := u.Query()
q.Set("package_name", dep.PackageName)
q.Set("package_type", strings.ToLower(dep.PackageType))
if dep.PackageVersion != nil && *dep.PackageVersion != "" {
q.Set("package_version", *dep.PackageVersion)
}
u.RawQuery = q.Encode()

return doRequest[v2types.Provenance](t.Options.HttpClient, u.String())
}

// doRequest only wraps (1) an HTTP GET issued to the given URL using
// the given client, and (2) result deserialization.
func doRequest[T any](client netClient, fullurl string) (*T, error) {
Expand Down
1 change: 1 addition & 0 deletions pkg/v2/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Trusty interface {
Summary(context.Context, *types.Dependency) (*types.PackageSummaryAnnotation, error)
PackageMetadata(context.Context, *types.Dependency) (*types.TrustyPackageData, error)
Alternatives(context.Context, *types.Dependency) (*types.PackageAlternatives, error)
Provenance(context.Context, *types.Dependency) (*types.Provenance, error)
}

// New returns a new Trusty REST client
Expand Down
83 changes: 55 additions & 28 deletions pkg/v2/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
// Dependency represents request arguments for various endpoints.
type Dependency struct {
PackageName string
PackageType *string
PackageType string
PackageVersion *string
}

Expand All @@ -52,7 +52,7 @@ type PackageSummaryAnnotation struct {
// changes to `"complete"` once processed.
type Status string

var (
const (
// StatusInProgress represents a package being processed.
StatusInProgress Status = "in_progress"
// StatusComplete represents an already processed package.
Expand All @@ -67,9 +67,9 @@ func (t *Status) UnmarshalJSON(data []byte) error {
}

switch tmp {
case "in_progress":
case string(StatusInProgress):
*t = StatusInProgress
case "complete":
case string(StatusComplete):
*t = StatusComplete
default:
return fmt.Errorf("invalid status type: %s", tmp)
Expand All @@ -96,18 +96,18 @@ type SummaryDescription struct {
// was able to gather.
type ProvenanceType string

var (
// ProvenanceTypeVerifiedProvenance represents a fully
// verified provenance information.
ProvenanceTypeVerifiedProvenance ProvenanceType = "verified_provenance"
// ProvenanceTypeHistoricalProvenance represents a verified
// historical provenance information.
ProvenanceTypeHistoricalProvenance ProvenanceType = "historical_provenance_match"
const (
// ProvenanceTypeVerified represents a fully verified
// provenance information.
ProvenanceTypeVerified ProvenanceType = "verified_provenance_match"
// ProvenanceTypeHistorical represents a verified historical
// provenance information.
ProvenanceTypeHistorical ProvenanceType = "historical_provenance_match"
// ProvenanceTypeUnknown represents no provenance information.
ProvenanceTypeUnknown ProvenanceType = "unknown"
// ProvenanceTypeMismatched represents conflicting provenance
// information.
ProvenanceTypeMismatched ProvenanceType = "mismatched"
ProvenanceTypeMismatched ProvenanceType = "historical_provenance_mismatched"
)

//nolint:revive
Expand All @@ -118,13 +118,13 @@ func (t *ProvenanceType) UnmarshalJSON(data []byte) error {
}

switch tmp {
case "verified_provenance":
*t = ProvenanceTypeVerifiedProvenance
case "historical_provenance_match":
*t = ProvenanceTypeHistoricalProvenance
case "unknown":
case string(ProvenanceTypeVerified):
*t = ProvenanceTypeVerified
case string(ProvenanceTypeHistorical):
*t = ProvenanceTypeHistorical
case string(ProvenanceTypeUnknown):
*t = ProvenanceTypeUnknown
case "mismatched":
case string(ProvenanceTypeMismatched):
*t = ProvenanceTypeMismatched
default:
return fmt.Errorf("invalid provenance type: %s", tmp)
Expand All @@ -141,7 +141,7 @@ type PackageType string
// as they're added to Trusty. The downside of this is that sdk users
// must match new types manually until we add the case to the list.

var (
const (
// PackageTypePypi is the ecosystem of Python packages.
PackageTypePypi PackageType = "pypi"
// PackageTypeNpm is the ecosystem of JavaScript packages.
Expand All @@ -158,7 +158,7 @@ var (
// repository.
type PackageStatus string

var (
const (
// PackageStatusPending represents status pending
PackageStatusPending PackageStatus = "pending"
// PackageStatusInitial represents status initial
Expand All @@ -185,21 +185,21 @@ func (t *PackageStatus) UnmarshalJSON(data []byte) error {
}

switch tmp {
case "pending":
case string(PackageStatusPending):
*t = PackageStatusPending
case "initial":
case string(PackageStatusInitial):
*t = PackageStatusInitial
case "neighbours":
case string(PackageStatusNeighbours):
*t = PackageStatusNeighbours
case "complete":
case string(PackageStatusComplete):
*t = PackageStatusComplete
case "failed":
case string(PackageStatusFailed):
*t = PackageStatusFailed
case "scoring":
case string(PackageStatusScoring):
*t = PackageStatusScoring
case "propagate":
case string(PackageStatusPropagate):
*t = PackageStatusPropagate
case "deleted":
case string(PackageStatusDeleted):
*t = PackageStatusDeleted
default:
return fmt.Errorf("invalid package status type: %s", tmp)
Expand Down Expand Up @@ -318,3 +318,30 @@ type PackageBasicInfo struct {
Score *float64 `json:"score"`
IsMalicious bool `json:"is_malicious"`
}

// Provenance contains details about historical or cryptographically
// verifiable provenance.
type Provenance struct {
Historical HistoricalProvenance `json:"hp"`
Sigstore SigstoreProvenance `json:"sigstore"`
Sore *float64 `json:"score"`
}

// HistoricalProvenance contains the number of tags in the repo, the
// number of versions of the package, a count of the common tags and
// the ratio of tags to common as overlap.
type HistoricalProvenance struct {
Overlap float64 `json:"overlap"` // 92.23300970873787
Common float64 `json:"common"` // 95.0
Tags float64 `json:"tags"` // 103.0
Versions float64 `json:"versions"` // 152.0
}

// SigstoreProvenance contains details about Sigstore provenance.
type SigstoreProvenance struct {
SourceRepo string `json:"source_repo"`
Workflow string `json:"workflow"`
Issuer string `json:"issuer"`
TokenIssuer string `json:"token_issuer"`
Transparency string `json:"transparency"`
}

0 comments on commit 089f44e

Please sign in to comment.