diff --git a/cmd/guacone/cmd/files.go b/cmd/guacone/cmd/files.go index 56e2ba1a8a..7825395495 100644 --- a/cmd/guacone/cmd/files.go +++ b/cmd/guacone/cmd/files.go @@ -145,6 +145,7 @@ var filesCmd = &cobra.Command{ gotErr = true return fmt.Errorf("unable to ingest document: %w", err) } + totalSuccess += 1 return nil } diff --git a/internal/testing/e2e/e2e b/internal/testing/e2e/e2e index 2b0b186db5..42f2462626 100755 --- a/internal/testing/e2e/e2e +++ b/internal/testing/e2e/e2e @@ -36,6 +36,8 @@ popd echo @@@@ Starting up guac server in background go run "${GUAC_DIR}/cmd/guacgql" & +sleep 15 + echo @@@@ Ingesting guac-data into server go run "${GUAC_DIR}/cmd/guacone" collect files "${GUAC_DIR}/guac-data/docs/" diff --git a/internal/testing/mocks/backend.go b/internal/testing/mocks/backend.go index 3c935ef4f4..b222cf5b95 100644 --- a/internal/testing/mocks/backend.go +++ b/internal/testing/mocks/backend.go @@ -695,6 +695,21 @@ func (mr *MockBackendMockRecorder) IngestPointOfContact(ctx, subject, pkgMatchTy return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IngestPointOfContact", reflect.TypeOf((*MockBackend)(nil).IngestPointOfContact), ctx, subject, pkgMatchType, pointOfContact) } +// IngestPointOfContacts mocks base method. +func (m *MockBackend) IngestPointOfContacts(ctx context.Context, subjects model.PackageSourceOrArtifactInputs, pkgMatchType *model.MatchFlags, pointOfContacts []*model.PointOfContactInputSpec) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IngestPointOfContacts", ctx, subjects, pkgMatchType, pointOfContacts) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IngestPointOfContacts indicates an expected call of IngestPointOfContacts. +func (mr *MockBackendMockRecorder) IngestPointOfContacts(ctx, subjects, pkgMatchType, pointOfContacts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IngestPointOfContacts", reflect.TypeOf((*MockBackend)(nil).IngestPointOfContacts), ctx, subjects, pkgMatchType, pointOfContacts) +} + // IngestSLSA mocks base method. func (m *MockBackend) IngestSLSA(ctx context.Context, subject model.ArtifactInputSpec, builtFrom []*model.ArtifactInputSpec, builtBy model.BuilderInputSpec, slsa model.SLSAInputSpec) (*model.HasSlsa, error) { m.ctrl.T.Helper() diff --git a/internal/testing/testdata/testdata.go b/internal/testing/testdata/testdata.go index d85b239fa1..e4662fa05c 100644 --- a/internal/testing/testdata/testdata.go +++ b/internal/testing/testdata/testdata.go @@ -734,10 +734,77 @@ var ( }, } + SpdxCertifyLegal = []assembler.CertifyLegalIngest{ + { + Pkg: baselayoutPack, + Declared: []model.LicenseInputSpec{ + { + Name: "GPL-2.0-only", + ListVersion: ptrfrom.String("3.18"), + }, + }, + Discovered: []model.LicenseInputSpec{ + { + Name: "GPL-2.0-only", + ListVersion: ptrfrom.String("3.18"), + }, + }, + CertifyLegal: &model.CertifyLegalInputSpec{ + DeclaredLicense: "GPL-2.0-only", + DiscoveredLicense: "GPL-2.0-only", + Justification: "Found in SPDX document.", + TimeScanned: parseRfc3339("2022-09-24T17:27:55.556104Z"), + }, + }, + { + Pkg: baselayoutdataPack, + Declared: []model.LicenseInputSpec{ + { + Name: "GPL-2.0-only", + ListVersion: ptrfrom.String("3.18"), + }, + }, + Discovered: []model.LicenseInputSpec{ + { + Name: "GPL-2.0-only", + ListVersion: ptrfrom.String("3.18"), + }, + }, + CertifyLegal: &model.CertifyLegalInputSpec{ + DeclaredLicense: "GPL-2.0-only", + DiscoveredLicense: "GPL-2.0-only", + Justification: "Found in SPDX document.", + TimeScanned: parseRfc3339("2022-09-24T17:27:55.556104Z"), + }, + }, + { + Pkg: keysPack, + Declared: []model.LicenseInputSpec{ + { + Name: "MIT", + ListVersion: ptrfrom.String("3.18"), + }, + }, + Discovered: []model.LicenseInputSpec{ + { + Name: "MIT", + ListVersion: ptrfrom.String("3.18"), + }, + }, + CertifyLegal: &model.CertifyLegalInputSpec{ + DeclaredLicense: "MIT", + DiscoveredLicense: "MIT", + Justification: "Found in SPDX document.", + TimeScanned: parseRfc3339("2022-09-24T17:27:55.556104Z"), + }, + }, + } + SpdxIngestionPredicates = assembler.IngestPredicates{ IsDependency: SpdxDeps, IsOccurrence: SpdxOccurences, HasSBOM: SpdxHasSBOM, + CertifyLegal: SpdxCertifyLegal, } // CycloneDX Testdata @@ -2613,6 +2680,8 @@ var IngestPredicatesCmpOpts = []cmp.Option{ cmpopts.SortSlices(packageQualifierInputSpecLess), cmpopts.SortSlices(psaInputSpecLess), cmpopts.SortSlices(slsaPredicateInputSpecLess), + cmpopts.SortSlices(certifyLegalInputSpecLess), + cmpopts.SortSlices(licenseInputSpecLess), } func certifyScorecardLess(e1, e2 assembler.CertifyScorecardIngest) bool { @@ -2639,6 +2708,14 @@ func slsaPredicateInputSpecLess(e1, e2 model.SLSAPredicateInputSpec) bool { return gLess(e1, e2) } +func certifyLegalInputSpecLess(e1, e2 assembler.CertifyLegalIngest) bool { + return gLess(e1, e2) +} + +func licenseInputSpecLess(e1, e2 generated.LicenseInputSpec) bool { + return gLess(e1, e2) +} + func gLess(e1, e2 any) bool { s1, _ := json.Marshal(e1) s2, _ := json.Marshal(e2) diff --git a/pkg/assembler/assembler.go b/pkg/assembler/assembler.go index cf6cc5a931..cd742043b5 100644 --- a/pkg/assembler/assembler.go +++ b/pkg/assembler/assembler.go @@ -47,6 +47,7 @@ type IngestPredicates struct { PointOfContact []PointOfContactIngest `json:"contact,omitempty"` VulnMetadata []VulnMetadataIngest `json:"vulnMetadata,omitempty"` HasMetadata []HasMetadataIngest `json:"hasMetadata,omitempty"` + CertifyLegal []CertifyLegalIngest `json:"certifyLegal,omitempty"` } type CertifyScorecardIngest struct { @@ -184,6 +185,16 @@ type PkgEqualIngest struct { PkgEqual *generated.PkgEqualInputSpec `json:"pkgEqual,omitempty"` } +type CertifyLegalIngest struct { + Pkg *generated.PkgInputSpec `json:"pkg,omitempty"` + Src *generated.SourceInputSpec `json:"src,omitempty"` + + Declared []generated.LicenseInputSpec `json:"declared,omitempty"` + Discovered []generated.LicenseInputSpec `json:"discovered,omitempty"` + + CertifyLegal *generated.CertifyLegalInputSpec `json:"certifyLegal,omitempty"` +} + func (i IngestPredicates) GetPackages(ctx context.Context) []*generated.PkgInputSpec { packageMap := make(map[string]*generated.PkgInputSpec) for _, dep := range i.IsDependency { @@ -286,6 +297,14 @@ func (i IngestPredicates) GetPackages(ctx context.Context) []*generated.PkgInput } } } + for _, cl := range i.CertifyLegal { + if cl.Pkg != nil { + pkgPurl := helpers.PkgInputSpecToPurl(cl.Pkg) + if _, ok := packageMap[pkgPurl]; !ok { + packageMap[pkgPurl] = cl.Pkg + } + } + } packages := make([]*generated.PkgInputSpec, 0, len(packageMap)) for _, pkg := range packageMap { @@ -352,6 +371,14 @@ func (i IngestPredicates) GetSources(ctx context.Context) []*generated.SourceInp } } } + for _, cl := range i.CertifyLegal { + if cl.Src != nil { + sourceString := concatenateSourceInput(cl.Src) + if _, ok := sourceMap[sourceString]; !ok { + sourceMap[sourceString] = cl.Src + } + } + } sources := make([]*generated.SourceInputSpec, 0, len(sourceMap)) for _, source := range sourceMap { @@ -527,6 +554,29 @@ func (i IngestPredicates) GetVulnerabilities(ctx context.Context) []*generated.V return vulns } +func (i IngestPredicates) GetLicenses(ctx context.Context) []generated.LicenseInputSpec { + licenseMap := make(map[string]*generated.LicenseInputSpec) + for _, cl := range i.CertifyLegal { + for i := range cl.Declared { + k := licenseKey(&cl.Declared[i]) + if _, ok := licenseMap[k]; !ok { + licenseMap[k] = &cl.Declared[i] + } + } + for i := range cl.Discovered { + k := licenseKey(&cl.Discovered[i]) + if _, ok := licenseMap[k]; !ok { + licenseMap[k] = &cl.Discovered[i] + } + } + } + licenses := make([]generated.LicenseInputSpec, 0, len(licenseMap)) + for _, license := range licenseMap { + licenses = append(licenses, *license) + } + return licenses +} + func concatenateSourceInput(source *generated.SourceInputSpec) string { var sourceElements []string sourceElements = append(sourceElements, source.Type, source.Namespace, source.Name) @@ -539,5 +589,12 @@ func concatenateSourceInput(source *generated.SourceInputSpec) string { return strings.Join(sourceElements, "/") } +func licenseKey(l *generated.LicenseInputSpec) string { + if l.ListVersion != nil && *l.ListVersion != "" { + return strings.Join([]string{l.Name, *l.ListVersion}, ":") + } + return l.Name +} + // AssemblerInput represents the inputs to add to the graph type AssemblerInput = IngestPredicates diff --git a/pkg/assembler/assembler_test.go b/pkg/assembler/assembler_test.go index b0a4246ab0..d63a4fc56e 100644 --- a/pkg/assembler/assembler_test.go +++ b/pkg/assembler/assembler_test.go @@ -84,6 +84,7 @@ func TestIngestPredicates(t *testing.T) { wantMaterials []generated.ArtifactInputSpec wantBuilder []*generated.BuilderInputSpec wantVuln []*generated.VulnerabilityInputSpec + wantLicense []generated.LicenseInputSpec }{{ name: "get nouns", field: IngestPredicates{ @@ -627,6 +628,60 @@ func TestIngestPredicates(t *testing.T) { }, }, }, + CertifyLegal: []CertifyLegalIngest{ + { + Pkg: maven, + Declared: []generated.LicenseInputSpec{ + { + Name: "asdf", + ListVersion: ptrfrom.String("1.2.3"), + }, + }, + Discovered: []generated.LicenseInputSpec{ + { + Name: "asdf", + ListVersion: ptrfrom.String("1.2.3"), + }, + { + Name: "qwer", + ListVersion: ptrfrom.String("1.2.3"), + }, + }, + CertifyLegal: &generated.CertifyLegalInputSpec{ + DeclaredLicense: "asdf", + DiscoveredLicense: "asdf AND qwer", + Attribution: "Copyright Jeff", + Justification: "Scanner foo", + TimeScanned: toTime("2022-10-06"), + }, + }, + { + Pkg: openSSL, + Declared: []generated.LicenseInputSpec{ + { + Name: "qwer", + ListVersion: ptrfrom.String("1.2.3"), + }, + }, + Discovered: []generated.LicenseInputSpec{ + { + Name: "qwer", + ListVersion: ptrfrom.String("1.2.3"), + }, + { + Name: "LicenseRef-123", + Inline: ptrfrom.String("This is the license text."), + }, + }, + CertifyLegal: &generated.CertifyLegalInputSpec{ + DeclaredLicense: "qwer", + DiscoveredLicense: "qwer AND LicenseRef-123", + Attribution: "Copyright Jeff", + Justification: "Scanner foo", + TimeScanned: toTime("2022-10-06"), + }, + }, + }, }, wantPkg: []*generated.PkgInputSpec{rootFilePack, maven, openSSL, openSSLWithQualifier, topLevelPack, baselayoutPack, baselayoutdataPack, worldFilePack}, wantSource: []*generated.SourceInputSpec{k8sSource}, @@ -733,6 +788,20 @@ func TestIngestPredicates(t *testing.T) { VulnerabilityID: "ghsa-p6xc-xr62-6r2g", }, }, + wantLicense: []generated.LicenseInputSpec{ + { + Name: "qwer", + ListVersion: ptrfrom.String("1.2.3"), + }, + { + Name: "asdf", + ListVersion: ptrfrom.String("1.2.3"), + }, + { + Name: "LicenseRef-123", + Inline: ptrfrom.String("This is the license text."), + }, + }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -776,6 +845,12 @@ func TestIngestPredicates(t *testing.T) { if diff := cmp.Diff(tt.wantVuln, gotVulns, cmpopts.SortSlices(vulnSort)); diff != "" { t.Errorf("Unexpected gotVulns results. (-want +got):\n%s", diff) } + + gotLicenses := i.GetLicenses(ctx) + licSort := func(a, b generated.LicenseInputSpec) bool { return a.Name < b.Name } + if diff := cmp.Diff(tt.wantLicense, gotLicenses, cmpopts.SortSlices(licSort)); diff != "" { + t.Errorf("Unexpected GetLicenses results. (-want +got):\n%s", diff) + } }) } } diff --git a/pkg/assembler/backends/arangodb/backend.go b/pkg/assembler/backends/arangodb/backend.go index 51375baa72..3ed1718900 100644 --- a/pkg/assembler/backends/arangodb/backend.go +++ b/pkg/assembler/backends/arangodb/backend.go @@ -107,6 +107,14 @@ const ( hasMetadataArtEdgesStr string = "hasMetadataArtEdges" hasMetadataStr string = "hasMetadataCollection" + // pointOfContact collection + + pointOfContactPkgVersionEdgesStr string = "pointOfContactPkgVersionEdges" + pointOfContactPkgNameEdgesStr string = "pointOfContactPkgNameEdges" + pointOfContactSrcEdgesStr string = "pointOfContactSrcEdges" + pointOfContactArtEdgesStr string = "pointOfContactArtEdges" + pointOfContactStr string = "pointOfContacts" + // hasSBOM collection hasSBOMPkgEdgesStr string = "hasSBOMPkgEdges" @@ -388,6 +396,27 @@ func getBackend(ctx context.Context, args backends.BackendArgs) (backends.Backen hasMetadataSrcEdges.From = []string{srcNamesStr} hasMetadataSrcEdges.To = []string{hasMetadataStr} + // setup pointOfContact collections + var pointOfContactPkgVersionEdges driver.EdgeDefinition + pointOfContactPkgVersionEdges.Collection = pointOfContactPkgVersionEdgesStr + pointOfContactPkgVersionEdges.From = []string{pkgVersionsStr} + pointOfContactPkgVersionEdges.To = []string{pointOfContactStr} + + var pointOfContactPkgNameEdges driver.EdgeDefinition + pointOfContactPkgNameEdges.Collection = pointOfContactPkgNameEdgesStr + pointOfContactPkgNameEdges.From = []string{pkgNamesStr} + pointOfContactPkgNameEdges.To = []string{pointOfContactStr} + + var pointOfContactArtEdges driver.EdgeDefinition + pointOfContactArtEdges.Collection = pointOfContactArtEdgesStr + pointOfContactArtEdges.From = []string{artifactsStr} + pointOfContactArtEdges.To = []string{pointOfContactStr} + + var pointOfContactSrcEdges driver.EdgeDefinition + pointOfContactSrcEdges.Collection = pointOfContactSrcEdgesStr + pointOfContactSrcEdges.From = []string{srcNamesStr} + pointOfContactSrcEdges.To = []string{pointOfContactStr} + // setup hasSBOM collections var hasSBOMPkgEdges driver.EdgeDefinition hasSBOMPkgEdges.Collection = hasSBOMPkgEdgesStr @@ -512,7 +541,8 @@ func getBackend(ctx context.Context, args backends.BackendArgs) (backends.Backen certifyBadArtEdges, certifyBadSrcEdges, certifyGoodPkgVersionEdges, certifyGoodPkgNameEdges, certifyGoodArtEdges, certifyGoodSrcEdges, certifyVexPkgEdges, certifyVexArtEdges, certifyVexVulnEdges, vulnMetadataEdges, vulnEqualVulnEdges, vulnEqualSubjectVulnEdges, pkgEqualPkgEdges, pkgEqualSubjectPkgEdges, hasMetadataPkgVersionEdges, hasMetadataPkgNameEdges, - hasMetadataArtEdges, hasMetadataSrcEdges} + hasMetadataArtEdges, hasMetadataSrcEdges, pointOfContactPkgVersionEdges, pointOfContactPkgNameEdges, + pointOfContactArtEdges, pointOfContactSrcEdges} // create a graph graph, err = db.CreateGraphV2(ctx, "guac", &options) diff --git a/pkg/assembler/backends/arangodb/hasMetadata.go b/pkg/assembler/backends/arangodb/hasMetadata.go index fa6f4b3ae0..b65e5af815 100644 --- a/pkg/assembler/backends/arangodb/hasMetadata.go +++ b/pkg/assembler/backends/arangodb/hasMetadata.go @@ -252,7 +252,7 @@ func getPkgHasMetadataForQuery(ctx context.Context, c *arangoClient, arangoQuery 'timestamp': hasMetadata.timestamp, 'justification': hasMetadata.justification, 'collector': hasMetadata.collector, - 'origin': hasMetadata.origin + 'origin': hasMetadata.origin }`) } diff --git a/pkg/assembler/backends/arangodb/pointOfContact.go b/pkg/assembler/backends/arangodb/pointOfContact.go index 0111504952..9671c463fd 100644 --- a/pkg/assembler/backends/arangodb/pointOfContact.go +++ b/pkg/assembler/backends/arangodb/pointOfContact.go @@ -17,15 +17,1005 @@ package arangodb import ( "context" + "encoding/json" "fmt" + "strings" + "time" + "github.com/arangodb/go-driver" "github.com/guacsec/guac/pkg/assembler/graphql/model" ) +const ( + emailStr string = "email" + infoStr string = "info" + sinceStr string = "since" +) + +func (c *arangoClient) PointOfContact(ctx context.Context, pointOfContactSpec *model.PointOfContactSpec) ([]*model.PointOfContact, error) { + + var arangoQueryBuilder *arangoQueryBuilder + if pointOfContactSpec.Subject != nil { + var combinedPointOfContact []*model.PointOfContact + if pointOfContactSpec.Subject.Package != nil { + values := map[string]any{} + // pkgVersion pointOfContact + arangoQueryBuilder = setPkgVersionMatchValues(pointOfContactSpec.Subject.Package, values) + arangoQueryBuilder.forOutBound(pointOfContactPkgVersionEdgesStr, "pointOfContact", "pVersion") + setPointOfContactMatchValues(arangoQueryBuilder, pointOfContactSpec, values) + + pkgVersionPointOfContact, err := getPkgPointOfContactForQuery(ctx, c, arangoQueryBuilder, values, true) + if err != nil { + return nil, fmt.Errorf("failed to retrieve package version pointOfContact with error: %w", err) + } + + combinedPointOfContact = append(combinedPointOfContact, pkgVersionPointOfContact...) + + if pointOfContactSpec.Subject.Package.ID == nil { + // pkgName pointOfContact + values = map[string]any{} + arangoQueryBuilder = setPkgNameMatchValues(pointOfContactSpec.Subject.Package, values) + arangoQueryBuilder.forOutBound(pointOfContactPkgNameEdgesStr, "pointOfContact", "pName") + setPointOfContactMatchValues(arangoQueryBuilder, pointOfContactSpec, values) + + pkgNamePointOfContact, err := getPkgPointOfContactForQuery(ctx, c, arangoQueryBuilder, values, false) + if err != nil { + return nil, fmt.Errorf("failed to retrieve package name pointOfContact with error: %w", err) + } + + combinedPointOfContact = append(combinedPointOfContact, pkgNamePointOfContact...) + } + } + if pointOfContactSpec.Subject.Source != nil { + values := map[string]any{} + arangoQueryBuilder = setSrcMatchValues(pointOfContactSpec.Subject.Source, values) + arangoQueryBuilder.forOutBound(pointOfContactSrcEdgesStr, "pointOfContact", "sName") + setPointOfContactMatchValues(arangoQueryBuilder, pointOfContactSpec, values) + + srcPointOfContact, err := getSrcPointOfContactForQuery(ctx, c, arangoQueryBuilder, values) + if err != nil { + return nil, fmt.Errorf("failed to retrieve source pointOfContact with error: %w", err) + } + + combinedPointOfContact = append(combinedPointOfContact, srcPointOfContact...) + } + if pointOfContactSpec.Subject.Artifact != nil { + values := map[string]any{} + arangoQueryBuilder = setArtifactMatchValues(pointOfContactSpec.Subject.Artifact, values) + arangoQueryBuilder.forOutBound(pointOfContactArtEdgesStr, "pointOfContact", "art") + setPointOfContactMatchValues(arangoQueryBuilder, pointOfContactSpec, values) + + artPointOfContact, err := getArtPointOfContactForQuery(ctx, c, arangoQueryBuilder, values) + if err != nil { + return nil, fmt.Errorf("failed to retrieve artifact pointOfContact with error: %w", err) + } + + combinedPointOfContact = append(combinedPointOfContact, artPointOfContact...) + } + return combinedPointOfContact, nil + } else { + values := map[string]any{} + var combinedPointOfContact []*model.PointOfContact + + // pkgVersion pointOfContact + arangoQueryBuilder = newForQuery(pointOfContactStr, "pointOfContact") + setPointOfContactMatchValues(arangoQueryBuilder, pointOfContactSpec, values) + arangoQueryBuilder.forInBound(pointOfContactPkgVersionEdgesStr, "pVersion", "pointOfContact") + arangoQueryBuilder.forInBound(pkgHasVersionStr, "pName", "pVersion") + arangoQueryBuilder.forInBound(pkgHasNameStr, "pNs", "pName") + arangoQueryBuilder.forInBound(pkgHasNamespaceStr, "pType", "pNs") + + pkgVersionPointOfContact, err := getPkgPointOfContactForQuery(ctx, c, arangoQueryBuilder, values, true) + if err != nil { + return nil, fmt.Errorf("failed to retrieve package version pointOfContact with error: %w", err) + } + combinedPointOfContact = append(combinedPointOfContact, pkgVersionPointOfContact...) + + // pkgName pointOfContact + values = map[string]any{} + arangoQueryBuilder = newForQuery(pointOfContactStr, "pointOfContact") + setPointOfContactMatchValues(arangoQueryBuilder, pointOfContactSpec, values) + arangoQueryBuilder.forInBound(pointOfContactPkgNameEdgesStr, "pName", "pointOfContact") + arangoQueryBuilder.forInBound(pkgHasNameStr, "pNs", "pName") + arangoQueryBuilder.forInBound(pkgHasNamespaceStr, "pType", "pNs") + + pkgNamePointOfContact, err := getPkgPointOfContactForQuery(ctx, c, arangoQueryBuilder, values, false) + if err != nil { + return nil, fmt.Errorf("failed to retrieve package name pointOfContact with error: %w", err) + } + combinedPointOfContact = append(combinedPointOfContact, pkgNamePointOfContact...) + + // get sources + values = map[string]any{} + arangoQueryBuilder = newForQuery(pointOfContactStr, "pointOfContact") + setPointOfContactMatchValues(arangoQueryBuilder, pointOfContactSpec, values) + arangoQueryBuilder.forInBound(pointOfContactSrcEdgesStr, "sName", "pointOfContact") + arangoQueryBuilder.forInBound(srcHasNameStr, "sNs", "sName") + arangoQueryBuilder.forInBound(srcHasNamespaceStr, "sType", "sNs") + + srcPointOfContact, err := getSrcPointOfContactForQuery(ctx, c, arangoQueryBuilder, values) + if err != nil { + return nil, fmt.Errorf("failed to retrieve source pointOfContact with error: %w", err) + } + combinedPointOfContact = append(combinedPointOfContact, srcPointOfContact...) + + // get artifacts + values = map[string]any{} + arangoQueryBuilder = newForQuery(pointOfContactStr, "pointOfContact") + setPointOfContactMatchValues(arangoQueryBuilder, pointOfContactSpec, values) + arangoQueryBuilder.forInBound(pointOfContactArtEdgesStr, "art", "pointOfContact") + + artPointOfContact, err := getArtPointOfContactForQuery(ctx, c, arangoQueryBuilder, values) + if err != nil { + return nil, fmt.Errorf("failed to retrieve artifact pointOfContact with error: %w", err) + } + combinedPointOfContact = append(combinedPointOfContact, artPointOfContact...) + + return combinedPointOfContact, nil + } +} + +func getSrcPointOfContactForQuery(ctx context.Context, c *arangoClient, arangoQueryBuilder *arangoQueryBuilder, values map[string]any) ([]*model.PointOfContact, error) { + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + 'srcName': { + 'type_id': sType._id, + 'type': sType.type, + 'namespace_id': sNs._id, + 'namespace': sNs.namespace, + 'name_id': sName._id, + 'name': sName.name, + 'commit': sName.commit, + 'tag': sName.tag + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }`) + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "PointOfContact") + if err != nil { + return nil, fmt.Errorf("failed to query for PointOfContact: %w", err) + } + defer cursor.Close() + + return getPointOfContactFromCursor(ctx, cursor) +} + +func getArtPointOfContactForQuery(ctx context.Context, c *arangoClient, arangoQueryBuilder *arangoQueryBuilder, values map[string]any) ([]*model.PointOfContact, error) { + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + 'artifact': { + 'id': art._id, + 'algorithm': art.algorithm, + 'digest': art.digest + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }`) + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "PointOfContact") + if err != nil { + return nil, fmt.Errorf("failed to query for PointOfContact: %w", err) + } + defer cursor.Close() + + return getPointOfContactFromCursor(ctx, cursor) +} + +func getPkgPointOfContactForQuery(ctx context.Context, c *arangoClient, arangoQueryBuilder *arangoQueryBuilder, values map[string]any, includeDepPkgVersion bool) ([]*model.PointOfContact, error) { + if includeDepPkgVersion { + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + 'pkgVersion': { + 'type_id': pType._id, + 'type': pType.type, + 'namespace_id': pNs._id, + 'namespace': pNs.namespace, + 'name_id': pName._id, + 'name': pName.name, + 'version_id': pVersion._id, + 'version': pVersion.version, + 'subpath': pVersion.subpath, + 'qualifier_list': pVersion.qualifier_list + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }`) + } else { + arangoQueryBuilder.query.WriteString("\n") + arangoQueryBuilder.query.WriteString(`RETURN { + 'pkgVersion': { + 'type_id': pType._id, + 'type': pType.type, + 'namespace_id': pNs._id, + 'namespace': pNs.namespace, + 'name_id': pName._id, + 'name': pName.name + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }`) + } + + cursor, err := executeQueryWithRetry(ctx, c.db, arangoQueryBuilder.string(), values, "PointOfContact") + if err != nil { + return nil, fmt.Errorf("failed to query for PointOfContact: %w", err) + } + defer cursor.Close() + + return getPointOfContactFromCursor(ctx, cursor) +} + +func setPointOfContactMatchValues(arangoQueryBuilder *arangoQueryBuilder, PointOfContactSpec *model.PointOfContactSpec, queryValues map[string]any) { + if PointOfContactSpec.ID != nil { + arangoQueryBuilder.filter("pointOfContact", "_id", "==", "@id") + queryValues["id"] = *PointOfContactSpec.ID + } + if PointOfContactSpec.Email != nil { + arangoQueryBuilder.filter("pointOfContact", emailStr, "==", "@"+emailStr) + queryValues[emailStr] = *PointOfContactSpec.Email + } + if PointOfContactSpec.Info != nil { + arangoQueryBuilder.filter("pointOfContact", infoStr, "==", "@"+infoStr) + queryValues[infoStr] = *PointOfContactSpec.Info + } + if PointOfContactSpec.Since != nil { + arangoQueryBuilder.filter("pointOfContact", sinceStr, ">=", "@"+sinceStr) + queryValues[sinceStr] = *PointOfContactSpec.Since + } + if PointOfContactSpec.Justification != nil { + arangoQueryBuilder.filter("pointOfContact", justification, "==", "@"+justification) + queryValues[justification] = *PointOfContactSpec.Justification + } + if PointOfContactSpec.Origin != nil { + arangoQueryBuilder.filter("pointOfContact", origin, "==", "@"+origin) + queryValues[origin] = *PointOfContactSpec.Origin + } + if PointOfContactSpec.Collector != nil { + arangoQueryBuilder.filter("pointOfContact", collector, "==", "@"+collector) + queryValues[collector] = *PointOfContactSpec.Collector + } +} + +func getPointOfContactQueryValues(pkg *model.PkgInputSpec, pkgMatchType *model.MatchFlags, artifact *model.ArtifactInputSpec, source *model.SourceInputSpec, pointOfContact *model.PointOfContactInputSpec) map[string]any { + values := map[string]any{} + // add guac keys + if pkg != nil { + pkgId := guacPkgId(*pkg) + if pkgMatchType.Pkg == model.PkgMatchTypeAllVersions { + values["pkgNameGuacKey"] = pkgId.NameId + } else { + values["pkgVersionGuacKey"] = pkgId.VersionId + } + } else if artifact != nil { + values["art_algorithm"] = strings.ToLower(artifact.Algorithm) + values["art_digest"] = strings.ToLower(artifact.Digest) + } else { + source := guacSrcId(*source) + values["srcNameGuacKey"] = source.NameId + } + + values[emailStr] = pointOfContact.Email + values[infoStr] = pointOfContact.Info + values[sinceStr] = pointOfContact.Since + values[justification] = pointOfContact.Justification + values[origin] = pointOfContact.Origin + values[collector] = pointOfContact.Collector + + return values +} + func (c *arangoClient) IngestPointOfContact(ctx context.Context, subject model.PackageSourceOrArtifactInput, pkgMatchType *model.MatchFlags, pointOfContact model.PointOfContactInputSpec) (*model.PointOfContact, error) { - return nil, fmt.Errorf("not implemented: IngestPointOfContact") + if subject.Package != nil { + if pkgMatchType.Pkg == model.PkgMatchTypeSpecificVersion { + query := ` + LET firstPkg = FIRST( + FOR pVersion in pkgVersions + FILTER pVersion.guacKey == @pkgVersionGuacKey + FOR pName in pkgNames + FILTER pName._id == pVersion._parent + FOR pNs in pkgNamespaces + FILTER pNs._id == pName._parent + FOR pType in pkgTypes + FILTER pType._id == pNs._parent + + RETURN { + 'typeID': pType._id, + 'type': pType.type, + 'namespace_id': pNs._id, + 'namespace': pNs.namespace, + 'name_id': pName._id, + 'name': pName.name, + 'version_id': pVersion._id, + 'version': pVersion.version, + 'subpath': pVersion.subpath, + 'qualifier_list': pVersion.qualifier_list, + 'versionDoc': pVersion + } + ) + + LET pointOfContact = FIRST( + UPSERT { packageID:firstPkg.version_id, email:@email, info:@info, since:@since, justification:@justification, collector:@collector, origin:@origin } + INSERT { packageID:firstPkg.version_id, email:@email, info:@info, since:@since, justification:@justification, collector:@collector, origin:@origin } + UPDATE {} IN pointOfContacts + RETURN NEW + ) + + LET edgeCollection = ( + INSERT { _key: CONCAT("pointOfContactPkgVersionEdges", firstPkg.versionDoc._key, pointOfContact._key), _from: firstPkg.version_id, _to: pointOfContact._id } INTO pointOfContactPkgVersionEdges OPTIONS { overwriteMode: "ignore" } + ) + + RETURN { + 'pkgVersion': { + 'type_id': firstPkg.typeID, + 'type': firstPkg.type, + 'namespace_id': firstPkg.namespace_id, + 'namespace': firstPkg.namespace, + 'name_id': firstPkg.name_id, + 'name': firstPkg.name, + 'version_id': firstPkg.version_id, + 'version': firstPkg.version, + 'subpath': firstPkg.subpath, + 'qualifier_list': firstPkg.qualifier_list + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }` + + cursor, err := executeQueryWithRetry(ctx, c.db, query, getPointOfContactQueryValues(subject.Package, pkgMatchType, nil, nil, &pointOfContact), "IngestPointOfContact - PkgVersion") + if err != nil { + return nil, fmt.Errorf("failed to ingest package pointOfContact: %w", err) + } + defer cursor.Close() + + pointOfContacts, err := getPointOfContactFromCursor(ctx, cursor) + if err != nil { + return nil, fmt.Errorf("failed to get pointOfContact from arango cursor: %w", err) + } + + if len(pointOfContacts) == 1 { + return pointOfContacts[0], nil + } else { + return nil, fmt.Errorf("number of pointOfContact ingested is greater than one") + } + } else { + query := ` + LET firstPkg = FIRST( + FOR pName in pkgNames + FILTER pName.guacKey == @pkgNameGuacKey + FOR pNs in pkgNamespaces + FILTER pNs._id == pName._parent + FOR pType in pkgTypes + FILTER pType._id == pNs._parent + + RETURN { + 'typeID': pType._id, + 'type': pType.type, + 'namespace_id': pNs._id, + 'namespace': pNs.namespace, + 'name_id': pName._id, + 'name': pName.name, + 'nameDoc': pName + } + ) + + LET pointOfContact = FIRST( + UPSERT { packageID:firstPkg.name_id, email:@email, info:@info, since:@since, justification:@justification, collector:@collector, origin:@origin } + INSERT { packageID:firstPkg.name_id, email:@email, info:@info, since:@since, justification:@justification, collector:@collector, origin:@origin } + UPDATE {} IN pointOfContacts + RETURN NEW + ) + + LET edgeCollection = ( + INSERT { _key: CONCAT("pointOfContactPkgNameEdges", firstPkg.nameDoc._key, pointOfContact._key), _from: firstPkg.name_id, _to: pointOfContact._id } INTO pointOfContactPkgNameEdges OPTIONS { overwriteMode: "ignore" } + ) + + RETURN { + 'pkgVersion': { + 'type_id': firstPkg.typeID, + 'type': firstPkg.type, + 'namespace_id': firstPkg.namespace_id, + 'namespace': firstPkg.namespace, + 'name_id': firstPkg.name_id, + 'name': firstPkg.name + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }` + + cursor, err := executeQueryWithRetry(ctx, c.db, query, getPointOfContactQueryValues(subject.Package, pkgMatchType, nil, nil, &pointOfContact), "IngestPointOfContact - PkgName") + if err != nil { + return nil, fmt.Errorf("failed to ingest package pointOfContact: %w", err) + } + defer cursor.Close() + + pointOfContacts, err := getPointOfContactFromCursor(ctx, cursor) + if err != nil { + return nil, fmt.Errorf("failed to get pointOfContact from arango cursor: %w", err) + } + + if len(pointOfContacts) == 1 { + return pointOfContacts[0], nil + } else { + return nil, fmt.Errorf("number of pointOfContact ingested is greater than one") + } + } + + } else if subject.Artifact != nil { + query := `LET artifact = FIRST(FOR art IN artifacts FILTER art.algorithm == @art_algorithm FILTER art.digest == @art_digest RETURN art) + + LET pointOfContact = FIRST( + UPSERT { artifactID:artifact._id, email:@email, info:@info, since:@since, justification:@justification, collector:@collector, origin:@origin } + INSERT { artifactID:artifact._id, email:@email, info:@info, since:@since, justification:@justification, collector:@collector, origin:@origin } + UPDATE {} IN pointOfContacts + RETURN NEW + ) + + LET edgeCollection = ( + INSERT { _key: CONCAT("pointOfContactArtEdges", artifact._key, pointOfContact._key), _from: artifact._id, _to: pointOfContact._id } INTO pointOfContactArtEdges OPTIONS { overwriteMode: "ignore" } + ) + + RETURN { + 'artifact': { + 'id': artifact._id, + 'algorithm': artifact.algorithm, + 'digest': artifact.digest + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }` + + cursor, err := executeQueryWithRetry(ctx, c.db, query, getPointOfContactQueryValues(nil, nil, subject.Artifact, nil, &pointOfContact), "IngestPointOfContact - artifact") + if err != nil { + return nil, fmt.Errorf("failed to ingest artifact pointOfContact: %w", err) + } + defer cursor.Close() + pointOfContacts, err := getPointOfContactFromCursor(ctx, cursor) + if err != nil { + return nil, fmt.Errorf("failed to get pointOfContact from arango cursor: %w", err) + } + + if len(pointOfContacts) == 1 { + return pointOfContacts[0], nil + } else { + return nil, fmt.Errorf("number of pointOfContact ingested is greater than one") + } + + } else if subject.Source != nil { + query := ` + LET firstSrc = FIRST( + FOR sName in srcNames + FILTER sName.guacKey == @srcNameGuacKey + FOR sNs in srcNamespaces + FILTER sNs._id == sName._parent + FOR sType in srcTypes + FILTER sType._id == sNs._parent + + RETURN { + 'typeID': sType._id, + 'type': sType.type, + 'namespace_id': sNs._id, + 'namespace': sNs.namespace, + 'name_id': sName._id, + 'name': sName.name, + 'commit': sName.commit, + 'tag': sName.tag, + 'nameDoc': sName + } + ) + + LET pointOfContact = FIRST( + UPSERT { sourceID:firstSrc.name_id, email:@email, info:@info, since:@since, justification:@justification, collector:@collector, origin:@origin } + INSERT { sourceID:firstSrc.name_id, email:@email, info:@info, since:@since, justification:@justification, collector:@collector, origin:@origin } + UPDATE {} IN pointOfContacts + RETURN NEW + ) + + LET edgeCollection = ( + INSERT { _key: CONCAT("pointOfContactSrcEdges", firstSrc.nameDoc._key, pointOfContact._key), _from: firstSrc.name_id, _to: pointOfContact._id } INTO pointOfContactSrcEdges OPTIONS { overwriteMode: "ignore" } + ) + + RETURN { + 'srcName': { + 'type_id': firstSrc.typeID, + 'type': firstSrc.type, + 'namespace_id': firstSrc.namespace_id, + 'namespace': firstSrc.namespace, + 'name_id': firstSrc.name_id, + 'name': firstSrc.name, + 'commit': firstSrc.commit, + 'tag': firstSrc.tag + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }` + + cursor, err := executeQueryWithRetry(ctx, c.db, query, getPointOfContactQueryValues(nil, nil, nil, subject.Source, &pointOfContact), "IngestPointOfContact - source") + if err != nil { + return nil, fmt.Errorf("failed to ingest source pointOfContact: %w", err) + } + defer cursor.Close() + pointOfContacts, err := getPointOfContactFromCursor(ctx, cursor) + if err != nil { + return nil, fmt.Errorf("failed to get pointOfContact from arango cursor: %w", err) + } + + if len(pointOfContacts) == 1 { + return pointOfContacts[0], nil + } else { + return nil, fmt.Errorf("number of pointOfContact ingested is greater than one") + } + + } else { + return nil, fmt.Errorf("package, artifact, or source is specified for IngestPointOfContact") + } } -func (c *arangoClient) PointOfContact(ctx context.Context, pointOfContactSpec *model.PointOfContactSpec) ([]*model.PointOfContact, error) { - return nil, fmt.Errorf("not implemented: PointOfContact") +func (c *arangoClient) IngestPointOfContacts(ctx context.Context, subjects model.PackageSourceOrArtifactInputs, pkgMatchType *model.MatchFlags, pointOfContacts []*model.PointOfContactInputSpec) ([]string, error) { + if len(subjects.Packages) > 0 { + var listOfValues []map[string]any + + for i := range subjects.Packages { + listOfValues = append(listOfValues, getPointOfContactQueryValues(subjects.Packages[i], pkgMatchType, nil, nil, pointOfContacts[i])) + } + + var documents []string + for _, val := range listOfValues { + bs, _ := json.Marshal(val) + documents = append(documents, string(bs)) + } + + queryValues := map[string]any{} + queryValues["documents"] = fmt.Sprint(strings.Join(documents, ",")) + + var sb strings.Builder + + sb.WriteString("for doc in [") + for i, val := range listOfValues { + bs, _ := json.Marshal(val) + if i == len(listOfValues)-1 { + sb.WriteString(string(bs)) + } else { + sb.WriteString(string(bs) + ",") + } + } + sb.WriteString("]") + + if pkgMatchType.Pkg == model.PkgMatchTypeSpecificVersion { + query := `LET firstPkg = FIRST( + FOR pVersion in pkgVersions + FILTER pVersion.guacKey == doc.pkgVersionGuacKey + FOR pName in pkgNames + FILTER pName._id == pVersion._parent + FOR pNs in pkgNamespaces + FILTER pNs._id == pName._parent + FOR pType in pkgTypes + FILTER pType._id == pNs._parent + + RETURN { + 'typeID': pType._id, + 'type': pType.type, + 'namespace_id': pNs._id, + 'namespace': pNs.namespace, + 'name_id': pName._id, + 'name': pName.name, + 'version_id': pVersion._id, + 'version': pVersion.version, + 'subpath': pVersion.subpath, + 'qualifier_list': pVersion.qualifier_list, + 'versionDoc': pVersion + } + ) + + LET pointOfContact = FIRST( + UPSERT { packageID:firstPkg.version_id, email:doc.email, info:doc.info, since:doc.since, justification:doc.justification, collector:doc.collector, origin:doc.origin } + INSERT { packageID:firstPkg.version_id, email:doc.email, info:doc.info, since:doc.since, justification:doc.justification, collector:doc.collector, origin:doc.origin } + UPDATE {} IN pointOfContacts + RETURN NEW + ) + + LET edgeCollection = ( + INSERT { _key: CONCAT("pointOfContactPkgVersionEdges", firstPkg.versionDoc._key, pointOfContact._key), _from: firstPkg.version_id, _to: pointOfContact._id } INTO pointOfContactPkgVersionEdges OPTIONS { overwriteMode: "ignore" } + ) + + RETURN { + 'pkgVersion': { + 'type_id': firstPkg.typeID, + 'type': firstPkg.type, + 'namespace_id': firstPkg.namespace_id, + 'namespace': firstPkg.namespace, + 'name_id': firstPkg.name_id, + 'name': firstPkg.name, + 'version_id': firstPkg.version_id, + 'version': firstPkg.version, + 'subpath': firstPkg.subpath, + 'qualifier_list': firstPkg.qualifier_list + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }` + + sb.WriteString(query) + + cursor, err := executeQueryWithRetry(ctx, c.db, sb.String(), nil, "IngestPointOfContacts - PkgVersion") + if err != nil { + return nil, fmt.Errorf("failed to ingest package pointOfContact: %w", err) + } + defer cursor.Close() + + ingestPointOfContactList, err := getPointOfContactFromCursor(ctx, cursor) + if err != nil { + return nil, fmt.Errorf("failed to get pointOfContact from arango cursor: %w", err) + } + + var pointOfContactIDList []string + for _, ingestedPointOfContact := range ingestPointOfContactList { + pointOfContactIDList = append(pointOfContactIDList, ingestedPointOfContact.ID) + } + + return pointOfContactIDList, nil + + } else { + query := ` + LET firstPkg = FIRST( + FOR pName in pkgNames + FILTER pName.guacKey == doc.pkgNameGuacKey + FOR pNs in pkgNamespaces + FILTER pNs._id == pName._parent + FOR pType in pkgTypes + FILTER pType._id == pNs._parent + + RETURN { + 'typeID': pType._id, + 'type': pType.type, + 'namespace_id': pNs._id, + 'namespace': pNs.namespace, + 'name_id': pName._id, + 'name': pName.name, + 'nameDoc': pName + } + ) + + LET pointOfContact = FIRST( + UPSERT { packageID:firstPkg.name_id, email:doc.email, info:doc.info, since:doc.since, justification:doc.justification, collector:doc.collector, origin:doc.origin } + INSERT { packageID:firstPkg.name_id, email:doc.email, info:doc.info, since:doc.since, justification:doc.justification, collector:doc.collector, origin:doc.origin } + UPDATE {} IN pointOfContacts + RETURN NEW + ) + + LET edgeCollection = ( + INSERT { _key: CONCAT("pointOfContactPkgNameEdges", firstPkg.nameDoc._key, pointOfContact._key), _from: firstPkg.name_id, _to: pointOfContact._id } INTO pointOfContactPkgNameEdges OPTIONS { overwriteMode: "ignore" } + ) + + RETURN { + 'pkgVersion': { + 'type_id': firstPkg.typeID, + 'type': firstPkg.type, + 'namespace_id': firstPkg.namespace_id, + 'namespace': firstPkg.namespace, + 'name_id': firstPkg.name_id, + 'name': firstPkg.name + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }` + + sb.WriteString(query) + + cursor, err := executeQueryWithRetry(ctx, c.db, sb.String(), nil, "IngestPointOfContacts - PkgName") + if err != nil { + return nil, fmt.Errorf("failed to ingest package pointOfContact: %w", err) + } + defer cursor.Close() + + ingestPointOfContactList, err := getPointOfContactFromCursor(ctx, cursor) + if err != nil { + return nil, fmt.Errorf("failed to get pointOfContact from arango cursor: %w", err) + } + + var pointOfContactIDList []string + for _, ingestedPointOfContact := range ingestPointOfContactList { + pointOfContactIDList = append(pointOfContactIDList, ingestedPointOfContact.ID) + } + + return pointOfContactIDList, nil + } + + } else if len(subjects.Artifacts) > 0 { + var listOfValues []map[string]any + + for i := range subjects.Artifacts { + listOfValues = append(listOfValues, getPointOfContactQueryValues(nil, nil, subjects.Artifacts[i], nil, pointOfContacts[i])) + } + + var documents []string + for _, val := range listOfValues { + bs, _ := json.Marshal(val) + documents = append(documents, string(bs)) + } + + queryValues := map[string]any{} + queryValues["documents"] = fmt.Sprint(strings.Join(documents, ",")) + + var sb strings.Builder + + sb.WriteString("for doc in [") + for i, val := range listOfValues { + bs, _ := json.Marshal(val) + if i == len(listOfValues)-1 { + sb.WriteString(string(bs)) + } else { + sb.WriteString(string(bs) + ",") + } + } + sb.WriteString("]") + + query := `LET artifact = FIRST(FOR art IN artifacts FILTER art.algorithm == doc.art_algorithm FILTER art.digest == doc.art_digest RETURN art) + + LET pointOfContact = FIRST( + UPSERT { artifactID:artifact._id, email:doc.email, info:doc.info, since:doc.since, justification:doc.justification, collector:doc.collector, origin:doc.origin } + INSERT { artifactID:artifact._id, email:doc.email, info:doc.info, since:doc.since, justification:doc.justification, collector:doc.collector, origin:doc.origin } + UPDATE {} IN pointOfContacts + RETURN NEW + ) + + LET edgeCollection = ( + INSERT { _key: CONCAT("pointOfContactArtEdges", artifact._key, pointOfContact._key), _from: artifact._id, _to: pointOfContact._id } INTO pointOfContactArtEdges OPTIONS { overwriteMode: "ignore" } + ) + + RETURN { + 'artifact': { + 'id': artifact._id, + 'algorithm': artifact.algorithm, + 'digest': artifact.digest + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }` + + sb.WriteString(query) + + cursor, err := executeQueryWithRetry(ctx, c.db, sb.String(), nil, "IngestPointOfContacts - artifact") + if err != nil { + return nil, fmt.Errorf("failed to ingest artifact pointOfContact: %w", err) + } + defer cursor.Close() + + ingestPointOfContactList, err := getPointOfContactFromCursor(ctx, cursor) + if err != nil { + return nil, fmt.Errorf("failed to get pointOfContact from arango cursor: %w", err) + } + + var pointOfContactIDList []string + for _, ingestedPointOfContact := range ingestPointOfContactList { + pointOfContactIDList = append(pointOfContactIDList, ingestedPointOfContact.ID) + } + + return pointOfContactIDList, nil + + } else if len(subjects.Sources) > 0 { + var listOfValues []map[string]any + + for i := range subjects.Sources { + listOfValues = append(listOfValues, getPointOfContactQueryValues(nil, nil, nil, subjects.Sources[i], pointOfContacts[i])) + } + + var documents []string + for _, val := range listOfValues { + bs, _ := json.Marshal(val) + documents = append(documents, string(bs)) + } + + queryValues := map[string]any{} + queryValues["documents"] = fmt.Sprint(strings.Join(documents, ",")) + + var sb strings.Builder + + sb.WriteString("for doc in [") + for i, val := range listOfValues { + bs, _ := json.Marshal(val) + if i == len(listOfValues)-1 { + sb.WriteString(string(bs)) + } else { + sb.WriteString(string(bs) + ",") + } + } + sb.WriteString("]") + + query := ` + LET firstSrc = FIRST( + FOR sName in srcNames + FILTER sName.guacKey == doc.srcNameGuacKey + FOR sNs in srcNamespaces + FILTER sNs._id == sName._parent + FOR sType in srcTypes + FILTER sType._id == sNs._parent + + RETURN { + 'typeID': sType._id, + 'type': sType.type, + 'namespace_id': sNs._id, + 'namespace': sNs.namespace, + 'name_id': sName._id, + 'name': sName.name, + 'commit': sName.commit, + 'tag': sName.tag, + 'nameDoc': sName + } + ) + + LET pointOfContact = FIRST( + UPSERT { sourceID:firstSrc.name_id, email:doc.email, info:doc.info, since:doc.since, justification:doc.justification, collector:doc.collector, origin:doc.origin } + INSERT { sourceID:firstSrc.name_id, email:doc.email, info:doc.info, since:doc.since, justification:doc.justification, collector:doc.collector, origin:doc.origin } + UPDATE {} IN pointOfContacts + RETURN NEW + ) + + LET edgeCollection = ( + INSERT { _key: CONCAT("pointOfContactSrcEdges", firstSrc.nameDoc._key, pointOfContact._key), _from: firstSrc.name_id, _to: pointOfContact._id } INTO pointOfContactSrcEdges OPTIONS { overwriteMode: "ignore" } + ) + + RETURN { + 'srcName': { + 'type_id': firstSrc.typeID, + 'type': firstSrc.type, + 'namespace_id': firstSrc.namespace_id, + 'namespace': firstSrc.namespace, + 'name_id': firstSrc.name_id, + 'name': firstSrc.name, + 'commit': firstSrc.commit, + 'tag': firstSrc.tag + }, + 'pointOfContact_id': pointOfContact._id, + 'email': pointOfContact.email, + 'info': pointOfContact.info, + 'since': pointOfContact.since, + 'justification': pointOfContact.justification, + 'collector': pointOfContact.collector, + 'origin': pointOfContact.origin + }` + + sb.WriteString(query) + + cursor, err := executeQueryWithRetry(ctx, c.db, sb.String(), nil, "IngestPointOfContacts - source") + if err != nil { + return nil, fmt.Errorf("failed to ingest source pointOfContact: %w", err) + } + defer cursor.Close() + + ingestPointOfContactList, err := getPointOfContactFromCursor(ctx, cursor) + if err != nil { + return nil, fmt.Errorf("failed to get pointOfContact from arango cursor: %w", err) + } + + var pointOfContactIDList []string + for _, ingestedPointOfContact := range ingestPointOfContactList { + pointOfContactIDList = append(pointOfContactIDList, ingestedPointOfContact.ID) + } + + return pointOfContactIDList, nil + + } else { + return nil, fmt.Errorf("packages, artifacts, or sources not specified for IngestPointOfContacts") + } +} + +func getPointOfContactFromCursor(ctx context.Context, cursor driver.Cursor) ([]*model.PointOfContact, error) { + type collectedData struct { + PkgVersion *dbPkgVersion `json:"pkgVersion"` + Artifact *model.Artifact `json:"artifact"` + SrcName *dbSrcName `json:"srcName"` + PointOfContactID string `json:"pointOfContact_id"` + Email string `json:"email"` + Info string `json:"info"` + Since time.Time `json:"since"` + Justification string `json:"justification"` + Collector string `json:"collector"` + Origin string `json:"origin"` + } + + var createdValues []collectedData + for { + var doc collectedData + _, err := cursor.ReadDocument(ctx, &doc) + if err != nil { + if driver.IsNoMoreDocuments(err) { + break + } else { + return nil, fmt.Errorf("failed to pointOfContact from cursor: %w", err) + } + } else { + createdValues = append(createdValues, doc) + } + } + + var pocList []*model.PointOfContact + for _, createdValue := range createdValues { + var pkg *model.Package = nil + var src *model.Source = nil + if createdValue.PkgVersion != nil { + pkg = generateModelPackage(createdValue.PkgVersion.TypeID, createdValue.PkgVersion.PkgType, createdValue.PkgVersion.NamespaceID, createdValue.PkgVersion.Namespace, createdValue.PkgVersion.NameID, + createdValue.PkgVersion.Name, createdValue.PkgVersion.VersionID, createdValue.PkgVersion.Version, createdValue.PkgVersion.Subpath, createdValue.PkgVersion.QualifierList) + } else if createdValue.SrcName != nil { + src = generateModelSource(createdValue.SrcName.TypeID, createdValue.SrcName.SrcType, createdValue.SrcName.NamespaceID, createdValue.SrcName.Namespace, + createdValue.SrcName.NameID, createdValue.SrcName.Name, createdValue.SrcName.Commit, createdValue.SrcName.Tag) + } + + poc := &model.PointOfContact{ + ID: createdValue.PointOfContactID, + Email: createdValue.Email, + Info: createdValue.Info, + Since: createdValue.Since, + Justification: createdValue.Justification, + Origin: createdValue.Collector, + Collector: createdValue.Origin, + } + + if pkg != nil { + poc.Subject = pkg + } else if src != nil { + poc.Subject = src + } else if createdValue.Artifact != nil { + poc.Subject = createdValue.Artifact + } else { + return nil, fmt.Errorf("failed to get subject from cursor for pointOfContact") + } + pocList = append(pocList, poc) + } + return pocList, nil } diff --git a/pkg/assembler/backends/arangodb/pointOfContact_test.go b/pkg/assembler/backends/arangodb/pointOfContact_test.go index c684e6a212..ec53674ad4 100644 --- a/pkg/assembler/backends/arangodb/pointOfContact_test.go +++ b/pkg/assembler/backends/arangodb/pointOfContact_test.go @@ -17,645 +17,1149 @@ package arangodb -// TODO (pxp928): enable once implemented +import ( + "context" + "strings" + "testing" + "time" -// func TestPointOfContact(t *testing.T) { -// type call struct { -// Sub model.PackageSourceOrArtifactInput -// Match *model.MatchFlags -// HM *model.PointOfContactInputSpec -// } -// tests := []struct { -// Name string -// InPkg []*model.PkgInputSpec -// InSrc []*model.SourceInputSpec -// InArt []*model.ArtifactInputSpec -// Calls []call -// Query *model.PointOfContactSpec -// ExpHM []*model.PointOfContact -// ExpIngestErr bool -// ExpQueryErr bool -// }{ -// { -// Name: "HappyPath", -// InPkg: []*model.PkgInputSpec{p1}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Email: "a@b.com", -// Info: "info1", -// Since: time.Unix(1e9, 0), -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Email: ptrfrom.String("a@b.com"), -// Info: ptrfrom.String("info1"), -// Since: ptrfrom.Time(time.Unix(1e9, 0)), -// Justification: ptrfrom.String("test justification"), -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: p1out, -// Email: "a@b.com", -// Info: "info1", -// Since: time.Unix(1e9, 0), -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "HappyPath check time since", -// InPkg: []*model.PkgInputSpec{p1}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Email: "a@b.com", -// Info: "info1", -// Since: time.Unix(1e9, 0), -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Email: ptrfrom.String("a@b.com"), -// Info: ptrfrom.String("info1"), -// Since: ptrfrom.Time(time.Unix(1e8, 0)), -// Justification: ptrfrom.String("test justification"), -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: p1out, -// Email: "a@b.com", -// Info: "info1", -// Since: time.Unix(1e9, 0), -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "UnhappyPath check time since", -// InPkg: []*model.PkgInputSpec{p1}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Email: "a@b.com", -// Info: "info1", -// Since: time.Unix(1e9, 0), -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Email: ptrfrom.String("a@b.com"), -// Info: ptrfrom.String("info1"), -// Since: ptrfrom.Time(time.Unix(1e10, 0)), -// Justification: ptrfrom.String("test justification"), -// }, -// ExpHM: nil, -// }, -// { -// Name: "HappyPath All Version", -// InPkg: []*model.PkgInputSpec{p1}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeAllVersions, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Justification: ptrfrom.String("test justification"), -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: p1outName, -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "Ingest same twice", -// InPkg: []*model.PkgInputSpec{p1}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Justification: ptrfrom.String("test justification"), -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: p1out, -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "Ingest two different keys", -// InPkg: []*model.PkgInputSpec{p1}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Email: "a@b.com", -// Info: "info1", -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Email: "x@y.com", -// Info: "info2", -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Justification: ptrfrom.String("test justification"), -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: p1out, -// Email: "a@b.com", -// Info: "info1", -// Justification: "test justification", -// }, -// { -// Subject: p1out, -// Email: "x@y.com", -// Info: "info2", -// Justification: "test justification", -// }, -// }, -// }, + "github.com/google/go-cmp/cmp" + "github.com/guacsec/guac/internal/testing/ptrfrom" + "github.com/guacsec/guac/internal/testing/testdata" + "github.com/guacsec/guac/pkg/assembler/graphql/model" +) -// { -// Name: "Query on Justification", -// InPkg: []*model.PkgInputSpec{p1}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification one", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification two", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Justification: ptrfrom.String("test justification one"), -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: p1out, -// Justification: "test justification one", -// }, -// }, -// }, -// { -// Name: "Query on Package", -// InPkg: []*model.PkgInputSpec{p1, p2}, -// InSrc: []*model.SourceInputSpec{s1}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p2, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Source: s1, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Subject: &model.PackageSourceOrArtifactSpec{ -// Package: &model.PkgSpec{ -// Version: ptrfrom.String("2.11.1"), -// }, -// }, -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: p2out, -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "Query on Source", -// InPkg: []*model.PkgInputSpec{p1}, -// InSrc: []*model.SourceInputSpec{s1, s2}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Source: s1, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Source: s2, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Subject: &model.PackageSourceOrArtifactSpec{ -// Source: &model.SourceSpec{ -// Name: ptrfrom.String("bobsrepo"), -// }, -// }, -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: s2out, -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "Query on Artifact", -// InSrc: []*model.SourceInputSpec{s1}, -// InArt: []*model.ArtifactInputSpec{a1, a2}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Artifact: a1, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Artifact: a2, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Source: s1, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Subject: &model.PackageSourceOrArtifactSpec{ -// Artifact: &model.ArtifactSpec{ -// Algorithm: ptrfrom.String("sha1"), -// }, -// }, -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: a2out, -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "Query none", -// InArt: []*model.ArtifactInputSpec{a1, a2}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Artifact: a1, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Artifact: a2, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Subject: &model.PackageSourceOrArtifactSpec{ -// Artifact: &model.ArtifactSpec{ -// Algorithm: ptrfrom.String("asdf"), -// }, -// }, -// }, -// ExpHM: nil, -// }, -// { -// Name: "Query multiple", -// InSrc: []*model.SourceInputSpec{s1, s2}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Source: s1, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Source: s2, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Justification: ptrfrom.String("test justification"), -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: s1out, -// Justification: "test justification", -// }, -// { -// Subject: s2out, -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "Query Packages", -// InPkg: []*model.PkgInputSpec{p1, p2}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p2, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeSpecificVersion, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Package: p2, -// }, -// Match: &model.MatchFlags{ -// Pkg: model.PkgMatchTypeAllVersions, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// Subject: &model.PackageSourceOrArtifactSpec{ -// Package: &model.PkgSpec{ -// Version: ptrfrom.String("2.11.1"), -// }, -// }, -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: p2out, -// Justification: "test justification", -// }, -// { -// Subject: p1outName, -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "Query ID", -// InArt: []*model.ArtifactInputSpec{a1, a2}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Artifact: a1, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Artifact: a2, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// ID: ptrfrom.String("3"), -// }, -// ExpHM: []*model.PointOfContact{ -// { -// Subject: a1out, -// Justification: "test justification", -// }, -// }, -// }, -// { -// Name: "Ingest without subject", -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Artifact: a1, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// ExpIngestErr: true, -// }, -// { -// Name: "Query good ID", -// InSrc: []*model.SourceInputSpec{s1}, -// Calls: []call{ -// { -// Sub: model.PackageSourceOrArtifactInput{ -// Source: s1, -// }, -// HM: &model.PointOfContactInputSpec{ -// Justification: "test justification", -// }, -// }, -// }, -// Query: &model.PointOfContactSpec{ -// ID: ptrfrom.String("asdf"), -// }, -// ExpQueryErr: true, -// }, -// } -// ignoreID := cmp.FilterPath(func(p cmp.Path) bool { -// return strings.Compare(".ID", p[len(p)-1].String()) == 0 -// }, cmp.Ignore()) -// ctx := context.Background() -// for _, test := range tests { -// t.Run(test.Name, func(t *testing.T) { -// b, err := inmem.getBackend(nil) -// if err != nil { -// t.Fatalf("Could not instantiate testing backend: %v", err) -// } -// for _, p := range test.InPkg { -// if _, err := b.IngestPackage(ctx, *p); err != nil { -// t.Fatalf("Could not ingest package: %v", err) -// } -// } -// for _, s := range test.InSrc { -// if _, err := b.IngestSource(ctx, *s); err != nil { -// t.Fatalf("Could not ingest source: %v", err) -// } -// } -// for _, a := range test.InArt { -// if _, err := b.IngestArtifact(ctx, a); err != nil { -// t.Fatalf("Could not ingest artifact: %v", err) -// } -// } -// for _, o := range test.Calls { -// _, err := b.IngestPointOfContact(ctx, o.Sub, o.Match, *o.HM) -// if (err != nil) != test.ExpIngestErr { -// t.Fatalf("did not get expected ingest error, want: %v, got: %v", test.ExpIngestErr, err) -// } -// if err != nil { -// return -// } -// } -// got, err := b.PointOfContact(ctx, test.Query) -// if (err != nil) != test.ExpQueryErr { -// t.Fatalf("did not get expected query error, want: %v, got: %v", test.ExpQueryErr, err) -// } -// if err != nil { -// return -// } -// if diff := cmp.Diff(test.ExpHM, got, ignoreID); diff != "" { -// t.Errorf("Unexpected results. (-want +got):\n%s", diff) -// } -// }) -// } -// } +func TestPointOfContact(t *testing.T) { + ctx := context.Background() + arangArg := getArangoConfig() + err := deleteDatabase(ctx, arangArg) + if err != nil { + t.Fatalf("error deleting arango database: %v", err) + } + b, err := getBackend(ctx, arangArg) + if err != nil { + t.Fatalf("error creating arango backend: %v", err) + } + type call struct { + Sub model.PackageSourceOrArtifactInput + Match *model.MatchFlags + POC *model.PointOfContactInputSpec + } + tests := []struct { + Name string + InPkg []*model.PkgInputSpec + InSrc []*model.SourceInputSpec + InArt []*model.ArtifactInputSpec + Calls []call + Query *model.PointOfContactSpec + QueryID bool + QueryPkgID bool + QuerySourceID bool + QueryArtID bool + ExpPoc []*model.PointOfContact + ExpIngestErr bool + ExpQueryErr bool + }{ + { + Name: "HappyPath", + InPkg: []*model.PkgInputSpec{testdata.P1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Email: "a@b.com", + Info: "info1", + Since: time.Unix(1e9, 0), + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Email: ptrfrom.String("a@b.com"), + Info: ptrfrom.String("info1"), + Since: ptrfrom.Time(time.Unix(1e9, 0)), + Justification: ptrfrom.String("test justification"), + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P1out, + Email: "a@b.com", + Info: "info1", + Since: time.Unix(1e9, 0), + Justification: "test justification", + }, + }, + }, + { + Name: "HappyPath check time since", + InPkg: []*model.PkgInputSpec{testdata.P1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Email: "a@b.com", + Info: "info1", + Since: time.Unix(1e9, 0), + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Email: ptrfrom.String("a@b.com"), + Info: ptrfrom.String("info1"), + Since: ptrfrom.Time(time.Unix(1e8, 0)), + Justification: ptrfrom.String("test justification"), + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P1out, + Email: "a@b.com", + Info: "info1", + Since: time.Unix(1e9, 0), + Justification: "test justification", + }, + }, + }, + { + Name: "UnhappyPath check time since", + InPkg: []*model.PkgInputSpec{testdata.P1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Email: "a@b.com", + Info: "info1", + Since: time.Unix(1e9, 0), + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Email: ptrfrom.String("a@b.com"), + Info: ptrfrom.String("info1"), + Since: ptrfrom.Time(time.Unix(1e10, 0)), + Justification: ptrfrom.String("test justification"), + }, + ExpPoc: nil, + }, + { + Name: "HappyPath All Version", + InPkg: []*model.PkgInputSpec{testdata.P1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeAllVersions, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Justification: ptrfrom.String("test justification"), + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P1out, + Email: "a@b.com", + Info: "info1", + Since: time.Unix(1e9, 0), + Justification: "test justification", + }, + { + Subject: testdata.P1outName, + Justification: "test justification", + }, + }, + }, + { + Name: "Ingest same twice", + InPkg: []*model.PkgInputSpec{testdata.P3}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P3, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P3, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Package: &model.PkgSpec{ + Version: ptrfrom.String("2.11.1"), + Subpath: ptrfrom.String("saved_model_cli.py"), + }, + }, + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P3out, + Justification: "test justification", + }, + { + Subject: testdata.P1outName, + Justification: "test justification", + }, + }, + }, + { + Name: "Ingest two different emails - query email", + InPkg: []*model.PkgInputSpec{testdata.P1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Email: "a@b.com", + Info: "info1", + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Email: "x@y.com", + Info: "info2", + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Email: ptrfrom.String("x@y.com"), + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P1out, + Email: "x@y.com", + Info: "info2", + Justification: "test justification", + }, + }, + }, + { + Name: "Ingest two different infos - query info", + InPkg: []*model.PkgInputSpec{testdata.P1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Email: "a@b.com", + Info: "info1", + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Email: "x@y.com", + Info: "info2", + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Info: ptrfrom.String("info1"), + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P1out, + Email: "a@b.com", + Info: "info1", + Since: time.Unix(1e9, 0), + Justification: "test justification", + }, + { + Subject: testdata.P1out, + Email: "a@b.com", + Info: "info1", + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Justification", + InPkg: []*model.PkgInputSpec{testdata.P1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification one", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification two", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Justification: ptrfrom.String("test justification one"), + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P1out, + Justification: "test justification one", + }, + }, + }, + { + Name: "Query on Package", + InPkg: []*model.PkgInputSpec{testdata.P1, testdata.P4}, + InSrc: []*model.SourceInputSpec{testdata.S1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P4, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Source: testdata.S1, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Package: &model.PkgSpec{ + Type: ptrfrom.String("conan"), + Namespace: ptrfrom.String("openssl.org"), + Name: ptrfrom.String("openssl"), + }, + }, + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P4out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Package version ID", + InPkg: []*model.PkgInputSpec{testdata.P4}, + InSrc: []*model.SourceInputSpec{}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P4, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + QueryPkgID: true, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P4out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Source", + InPkg: []*model.PkgInputSpec{testdata.P1}, + InSrc: []*model.SourceInputSpec{testdata.S1, testdata.S2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Source: testdata.S1, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Source: testdata.S2, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Source: &model.SourceSpec{ + Name: ptrfrom.String("bobsrepo"), + }, + }, + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.S2out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Source ID", + InPkg: []*model.PkgInputSpec{}, + InSrc: []*model.SourceInputSpec{testdata.S2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Source: testdata.S2, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + QuerySourceID: true, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.S2out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Artifact", + InSrc: []*model.SourceInputSpec{testdata.S1}, + InArt: []*model.ArtifactInputSpec{testdata.A1, testdata.A2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Artifact: testdata.A1, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Artifact: testdata.A2, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Source: testdata.S1, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Artifact: &model.ArtifactSpec{ + Algorithm: ptrfrom.String("sha1"), + }, + }, + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.A2out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Artifact ID", + InSrc: []*model.SourceInputSpec{}, + InArt: []*model.ArtifactInputSpec{testdata.A1, testdata.A2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Artifact: testdata.A1, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Artifact: testdata.A2, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + QueryArtID: true, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.A2out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query none", + InArt: []*model.ArtifactInputSpec{testdata.A1, testdata.A2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Artifact: testdata.A1, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Artifact: testdata.A2, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Artifact: &model.ArtifactSpec{ + Algorithm: ptrfrom.String("asdf"), + }, + }, + }, + ExpPoc: nil, + }, + { + Name: "Query multiple", + InSrc: []*model.SourceInputSpec{testdata.S1, testdata.S2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Source: testdata.S1, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Source: testdata.S2, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Source: &model.SourceSpec{ + Type: ptrfrom.String("git"), + }, + }}, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.S2out, + Justification: "test justification", + }, + { + Subject: testdata.S1out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query Packages", + InPkg: []*model.PkgInputSpec{testdata.P1, testdata.P2, testdata.P4}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P1, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P2, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P2, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeAllVersions, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Package: testdata.P4, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeAllVersions, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Package: &model.PkgSpec{ + Name: ptrfrom.String("openssl"), + Version: ptrfrom.String("3.0.3"), + }, + }, + }, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.P4out, + Justification: "test justification", + }, + { + Subject: testdata.P4outName, + Justification: "test justification", + }, + }, + }, + { + Name: "Query ID", + InArt: []*model.ArtifactInputSpec{testdata.A1, testdata.A2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Artifact: testdata.A1, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + { + Sub: model.PackageSourceOrArtifactInput{ + Artifact: testdata.A2, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + QueryID: true, + ExpPoc: []*model.PointOfContact{ + { + Subject: testdata.A2out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query bad ID", + InSrc: []*model.SourceInputSpec{testdata.S1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInput{ + Source: testdata.S1, + }, + POC: &model.PointOfContactInputSpec{ + Justification: "test justification", + }, + }, + }, + Query: &model.PointOfContactSpec{ + ID: ptrfrom.String("asdf"), + }, + ExpQueryErr: false, + }, + } + ignoreID := cmp.FilterPath(func(p cmp.Path) bool { + return strings.Compare(".ID", p[len(p)-1].String()) == 0 + }, cmp.Ignore()) + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + for _, p := range test.InPkg { + if _, err := b.IngestPackage(ctx, *p); err != nil { + t.Fatalf("Could not ingest package: %v", err) + } + } + for _, s := range test.InSrc { + if _, err := b.IngestSource(ctx, *s); err != nil { + t.Fatalf("Could not ingest source: %v", err) + } + } + for _, a := range test.InArt { + if _, err := b.IngestArtifact(ctx, a); err != nil { + t.Fatalf("Could not ingest artifact: %v", err) + } + } + for _, o := range test.Calls { + found, err := b.IngestPointOfContact(ctx, o.Sub, o.Match, *o.POC) + if (err != nil) != test.ExpIngestErr { + t.Fatalf("did not get expected ingest error, want: %v, got: %v", test.ExpIngestErr, err) + } + if err != nil { + return + } + if test.QueryID { + test.Query = &model.PointOfContactSpec{ + ID: ptrfrom.String(found.ID), + } + } + if test.QueryPkgID { + if _, ok := found.Subject.(*model.Package); ok { + test.Query = &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Package: &model.PkgSpec{ + ID: ptrfrom.String(found.Subject.(*model.Package).Namespaces[0].Names[0].Versions[0].ID), + }, + }, + } + } + } + if test.QuerySourceID { + if _, ok := found.Subject.(*model.Source); ok { + test.Query = &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Source: &model.SourceSpec{ + ID: ptrfrom.String(found.Subject.(*model.Source).Namespaces[0].Names[0].ID), + }, + }, + } + } + } + if test.QueryArtID { + if _, ok := found.Subject.(*model.Artifact); ok { + test.Query = &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Artifact: &model.ArtifactSpec{ + ID: ptrfrom.String(found.Subject.(*model.Artifact).ID), + }, + }, + } + } + } + } + got, err := b.PointOfContact(ctx, test.Query) + if (err != nil) != test.ExpQueryErr { + t.Fatalf("did not get expected query error, want: %v, got: %v", test.ExpQueryErr, err) + } + if err != nil { + return + } + if diff := cmp.Diff(test.ExpPoc, got, ignoreID); diff != "" { + t.Errorf("Unexpected results. (-want +got):\n%s", diff) + } + }) + } +} + +func TestIngestPointOfContacts(t *testing.T) { + ctx := context.Background() + arangArg := getArangoConfig() + err := deleteDatabase(ctx, arangArg) + if err != nil { + t.Fatalf("error deleting arango database: %v", err) + } + b, err := getBackend(ctx, arangArg) + if err != nil { + t.Fatalf("error creating arango backend: %v", err) + } + type call struct { + Sub model.PackageSourceOrArtifactInputs + Match *model.MatchFlags + PC []*model.PointOfContactInputSpec + } + tests := []struct { + Name string + InPkg []*model.PkgInputSpec + InSrc []*model.SourceInputSpec + InArt []*model.ArtifactInputSpec + Calls []call + Query *model.PointOfContactSpec + ExpPC []*model.PointOfContact + ExpIngestErr bool + ExpQueryErr bool + }{ + { + Name: "HappyPath", + InPkg: []*model.PkgInputSpec{testdata.P1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{testdata.P1}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Justification: ptrfrom.String("test justification"), + }, + ExpPC: []*model.PointOfContact{ + { + Subject: testdata.P1out, + Justification: "test justification", + }, + }, + }, + { + Name: "HappyPath All Version", + InPkg: []*model.PkgInputSpec{testdata.P1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{testdata.P1}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeAllVersions, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Justification: ptrfrom.String("test justification"), + }, + ExpPC: []*model.PointOfContact{ + { + Subject: testdata.P1out, + Justification: "test justification", + }, + { + Subject: testdata.P1outName, + Justification: "test justification", + }, + }, + }, + { + Name: "Ingest same twice", + InPkg: []*model.PkgInputSpec{testdata.P3}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{testdata.P3, testdata.P3}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Package: &model.PkgSpec{ + Version: ptrfrom.String("2.11.1"), + Subpath: ptrfrom.String("saved_model_cli.py"), + }, + }, + }, + ExpPC: []*model.PointOfContact{ + { + Subject: testdata.P3out, + Justification: "test justification", + }, + { + Subject: testdata.P1outName, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Package", + InPkg: []*model.PkgInputSpec{testdata.P1, testdata.P4}, + InSrc: []*model.SourceInputSpec{testdata.S1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{testdata.P1, testdata.P4}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + { + Justification: "test justification", + }, + }, + }, + { + Sub: model.PackageSourceOrArtifactInputs{ + Sources: []*model.SourceInputSpec{testdata.S1}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Package: &model.PkgSpec{ + Type: ptrfrom.String("conan"), + Namespace: ptrfrom.String("openssl.org"), + Name: ptrfrom.String("openssl"), + }, + }, + }, + ExpPC: []*model.PointOfContact{ + { + Subject: testdata.P4out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Source", + InPkg: []*model.PkgInputSpec{testdata.P1}, + InSrc: []*model.SourceInputSpec{testdata.S1, testdata.S2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{testdata.P1}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + { + Sub: model.PackageSourceOrArtifactInputs{ + Sources: []*model.SourceInputSpec{testdata.S2, testdata.S2}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Source: &model.SourceSpec{ + Name: ptrfrom.String("bobsrepo"), + }, + }, + }, + ExpPC: []*model.PointOfContact{ + { + Subject: testdata.S2out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Artifact", + InSrc: []*model.SourceInputSpec{testdata.S1}, + InArt: []*model.ArtifactInputSpec{testdata.A1, testdata.A2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Artifacts: []*model.ArtifactInputSpec{testdata.A1, testdata.A2}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + { + Justification: "test justification", + }, + }, + }, + { + Sub: model.PackageSourceOrArtifactInputs{ + Sources: []*model.SourceInputSpec{testdata.S1}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Artifact: &model.ArtifactSpec{ + Algorithm: ptrfrom.String("sha1"), + }, + }, + }, + ExpPC: []*model.PointOfContact{ + { + Subject: testdata.A2out, + Justification: "test justification", + }, + }, + }, + } + ignoreID := cmp.FilterPath(func(p cmp.Path) bool { + return strings.Compare(".ID", p[len(p)-1].String()) == 0 + }, cmp.Ignore()) + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + for _, p := range test.InPkg { + if _, err := b.IngestPackage(ctx, *p); err != nil { + t.Fatalf("Could not ingest package: %v", err) + } + } + for _, s := range test.InSrc { + if _, err := b.IngestSource(ctx, *s); err != nil { + t.Fatalf("Could not ingest source: %v", err) + } + } + for _, a := range test.InArt { + if _, err := b.IngestArtifact(ctx, a); err != nil { + t.Fatalf("Could not ingest artifact: %v", err) + } + } + for _, o := range test.Calls { + _, err := b.IngestPointOfContacts(ctx, o.Sub, o.Match, o.PC) + if (err != nil) != test.ExpIngestErr { + t.Fatalf("did not get expected ingest error, want: %v, got: %v", test.ExpIngestErr, err) + } + if err != nil { + return + } + } + got, err := b.PointOfContact(ctx, test.Query) + if (err != nil) != test.ExpQueryErr { + t.Fatalf("did not get expected query error, want: %v, got: %v", test.ExpQueryErr, err) + } + if err != nil { + return + } + if diff := cmp.Diff(test.ExpPC, got, ignoreID); diff != "" { + t.Errorf("Unexpected results. (-want +got):\n%s", diff) + } + }) + } +} + +// TODO (pxp928): add tests back in when implemented // func TestPointOfContactNeighbors(t *testing.T) { // type call struct { @@ -673,11 +1177,11 @@ package arangodb // }{ // { // Name: "HappyPath", -// InPkg: []*model.PkgInputSpec{p1}, +// InPkg: []*model.PkgInputSpec{testdata.P1}, // Calls: []call{ // { // Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, +// Package: testdata.P1, // }, // Match: &model.MatchFlags{ // Pkg: model.PkgMatchTypeSpecificVersion, @@ -694,13 +1198,13 @@ package arangodb // }, // { // Name: "Pkg Name Src and Artifact", -// InPkg: []*model.PkgInputSpec{p1}, -// InSrc: []*model.SourceInputSpec{s1}, -// InArt: []*model.ArtifactInputSpec{a1}, +// InPkg: []*model.PkgInputSpec{testdata.P1}, +// InSrc: []*model.SourceInputSpec{testdata.S1}, +// InArt: []*model.ArtifactInputSpec{testdata.A1}, // Calls: []call{ // { // Sub: model.PackageSourceOrArtifactInput{ -// Package: p1, +// Package: testdata.P1, // }, // Match: &model.MatchFlags{ // Pkg: model.PkgMatchTypeAllVersions, @@ -711,7 +1215,7 @@ package arangodb // }, // { // Sub: model.PackageSourceOrArtifactInput{ -// Source: s1, +// Source: testdata.S1, // }, // HM: &model.PointOfContactInputSpec{ // Justification: "test justification", @@ -719,7 +1223,7 @@ package arangodb // }, // { // Sub: model.PackageSourceOrArtifactInput{ -// Artifact: a1, +// Artifact: testdata.A1, // }, // HM: &model.PointOfContactInputSpec{ // Justification: "test justification", @@ -744,7 +1248,7 @@ package arangodb // ctx := context.Background() // for _, test := range tests { // t.Run(test.Name, func(t *testing.T) { -// b, err := inmem.getBackend(nil) +// b, err := backends.Get("inmem", nil, nil) // if err != nil { // t.Fatalf("Could not instantiate testing backend: %v", err) // } diff --git a/pkg/assembler/backends/backends.go b/pkg/assembler/backends/backends.go index 3ec735bd8b..0733a0e567 100644 --- a/pkg/assembler/backends/backends.go +++ b/pkg/assembler/backends/backends.go @@ -89,6 +89,7 @@ type Backend interface { IngestPkgEqual(ctx context.Context, pkg model.PkgInputSpec, depPkg model.PkgInputSpec, pkgEqual model.PkgEqualInputSpec) (*model.PkgEqual, error) IngestPkgEquals(ctx context.Context, pkgs []*model.PkgInputSpec, otherPackages []*model.PkgInputSpec, pkgEquals []*model.PkgEqualInputSpec) ([]string, error) IngestPointOfContact(ctx context.Context, subject model.PackageSourceOrArtifactInput, pkgMatchType *model.MatchFlags, pointOfContact model.PointOfContactInputSpec) (*model.PointOfContact, error) + IngestPointOfContacts(ctx context.Context, subjects model.PackageSourceOrArtifactInputs, pkgMatchType *model.MatchFlags, pointOfContacts []*model.PointOfContactInputSpec) ([]string, error) IngestSLSA(ctx context.Context, subject model.ArtifactInputSpec, builtFrom []*model.ArtifactInputSpec, builtBy model.BuilderInputSpec, slsa model.SLSAInputSpec) (*model.HasSlsa, error) IngestSLSAs(ctx context.Context, subjects []*model.ArtifactInputSpec, builtFromList [][]*model.ArtifactInputSpec, builtByList []*model.BuilderInputSpec, slsaList []*model.SLSAInputSpec) ([]*model.HasSlsa, error) IngestScorecard(ctx context.Context, source model.SourceInputSpec, scorecard model.ScorecardInputSpec) (*model.CertifyScorecard, error) diff --git a/pkg/assembler/backends/inmem/certifyLegal.go b/pkg/assembler/backends/inmem/certifyLegal.go index 51602175c2..5d785ce9c8 100644 --- a/pkg/assembler/backends/inmem/certifyLegal.go +++ b/pkg/assembler/backends/inmem/certifyLegal.go @@ -131,7 +131,7 @@ func (c *demoClient) ingestCertifyLegal(ctx context.Context, subject model.Packa for _, lis := range declaredLicenses { l, ok := c.licenses[licenseKey(lis.Name, lis.ListVersion)] if !ok { - return nil, gqlerror.Errorf("%v :: License not found %s", funcName, lis.Name) + return nil, gqlerror.Errorf("%v :: License not found %q", funcName, licenseKey(lis.Name, lis.ListVersion)) } dec = append(dec, l.id) } @@ -140,7 +140,7 @@ func (c *demoClient) ingestCertifyLegal(ctx context.Context, subject model.Packa for _, lis := range discoveredLicenses { l, ok := c.licenses[licenseKey(lis.Name, lis.ListVersion)] if !ok { - return nil, gqlerror.Errorf("%v :: License not found %s", funcName, lis.Name) + return nil, gqlerror.Errorf("%v :: License not found %q", funcName, licenseKey(lis.Name, lis.ListVersion)) } dis = append(dis, l.id) } diff --git a/pkg/assembler/backends/inmem/isOccurrence_test.go b/pkg/assembler/backends/inmem/isOccurrence_test.go index ad9be1f8e9..1f6a603e0d 100644 --- a/pkg/assembler/backends/inmem/isOccurrence_test.go +++ b/pkg/assembler/backends/inmem/isOccurrence_test.go @@ -162,6 +162,19 @@ var p4 = &model.PkgInputSpec{ Name: "openssl", Version: ptrfrom.String("3.0.3"), } +var p4out = &model.Package{ + Type: "conan", + Namespaces: []*model.PackageNamespace{{ + Namespace: "openssl.org", + Names: []*model.PackageName{{ + Name: "openssl", + Versions: []*model.PackageVersion{{ + Version: "3.0.3", + Qualifiers: []*model.PackageQualifier{}, + }}, + }}, + }}, +} var p4outName = &model.Package{ Type: "conan", Namespaces: []*model.PackageNamespace{{ diff --git a/pkg/assembler/backends/inmem/pointOfContact.go b/pkg/assembler/backends/inmem/pointOfContact.go index 19eebbe8ba..4e046a22fa 100644 --- a/pkg/assembler/backends/inmem/pointOfContact.go +++ b/pkg/assembler/backends/inmem/pointOfContact.go @@ -61,6 +61,37 @@ func (n *pointOfContactLink) BuildModelNode(c *demoClient) (model.Node, error) { } // Ingest PointOfContact + +func (c *demoClient) IngestPointOfContacts(ctx context.Context, subjects model.PackageSourceOrArtifactInputs, pkgMatchType *model.MatchFlags, pointOfContacts []*model.PointOfContactInputSpec) ([]string, error) { + var modelPointOfContactIDs []string + + for i := range pointOfContacts { + var pointOfContact *model.PointOfContact + var err error + if len(subjects.Packages) > 0 { + subject := model.PackageSourceOrArtifactInput{Package: subjects.Packages[i]} + pointOfContact, err = c.IngestPointOfContact(ctx, subject, pkgMatchType, *pointOfContacts[i]) + if err != nil { + return nil, gqlerror.Errorf("IngestPointOfContact failed with err: %v", err) + } + } else if len(subjects.Sources) > 0 { + subject := model.PackageSourceOrArtifactInput{Source: subjects.Sources[i]} + pointOfContact, err = c.IngestPointOfContact(ctx, subject, pkgMatchType, *pointOfContacts[i]) + if err != nil { + return nil, gqlerror.Errorf("IngestPointOfContact failed with err: %v", err) + } + } else { + subject := model.PackageSourceOrArtifactInput{Artifact: subjects.Artifacts[i]} + pointOfContact, err = c.IngestPointOfContact(ctx, subject, pkgMatchType, *pointOfContacts[i]) + if err != nil { + return nil, gqlerror.Errorf("IngestPointOfContact failed with err: %v", err) + } + } + modelPointOfContactIDs = append(modelPointOfContactIDs, pointOfContact.ID) + } + return modelPointOfContactIDs, nil +} + func (c *demoClient) IngestPointOfContact(ctx context.Context, subject model.PackageSourceOrArtifactInput, pkgMatchType *model.MatchFlags, pointOfContact model.PointOfContactInputSpec) (*model.PointOfContact, error) { return c.ingestPointOfContact(ctx, subject, pkgMatchType, pointOfContact, true) } diff --git a/pkg/assembler/backends/inmem/pointOfContact_test.go b/pkg/assembler/backends/inmem/pointOfContact_test.go index 54b99a62a9..7e678dd09e 100644 --- a/pkg/assembler/backends/inmem/pointOfContact_test.go +++ b/pkg/assembler/backends/inmem/pointOfContact_test.go @@ -666,6 +666,302 @@ func TestPointOfContact(t *testing.T) { } } +func TestIngestPointOfContacts(t *testing.T) { + type call struct { + Sub model.PackageSourceOrArtifactInputs + Match *model.MatchFlags + PC []*model.PointOfContactInputSpec + } + tests := []struct { + Name string + InPkg []*model.PkgInputSpec + InSrc []*model.SourceInputSpec + InArt []*model.ArtifactInputSpec + Calls []call + Query *model.PointOfContactSpec + ExpPC []*model.PointOfContact + ExpIngestErr bool + ExpQueryErr bool + }{ + { + Name: "HappyPath", + InPkg: []*model.PkgInputSpec{p1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{p1}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Justification: ptrfrom.String("test justification"), + }, + ExpPC: []*model.PointOfContact{ + { + Subject: p1out, + Justification: "test justification", + }, + }, + }, + { + Name: "HappyPath All Version", + InPkg: []*model.PkgInputSpec{p1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{p1}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeAllVersions, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Justification: ptrfrom.String("test justification"), + }, + ExpPC: []*model.PointOfContact{ + { + Subject: p1outName, + Justification: "test justification", + }, + }, + }, + { + Name: "Ingest same twice", + InPkg: []*model.PkgInputSpec{p3}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{p3, p3}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Package: &model.PkgSpec{ + Version: ptrfrom.String("2.11.1"), + Subpath: ptrfrom.String("saved_model_cli.py"), + }, + }, + }, + ExpPC: []*model.PointOfContact{ + { + Subject: p3out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Package", + InPkg: []*model.PkgInputSpec{p1, p4}, + InSrc: []*model.SourceInputSpec{s1}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{p1, p4}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + { + Justification: "test justification", + }, + }, + }, + { + Sub: model.PackageSourceOrArtifactInputs{ + Sources: []*model.SourceInputSpec{s1}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Package: &model.PkgSpec{ + Type: ptrfrom.String("conan"), + Namespace: ptrfrom.String("openssl.org"), + Name: ptrfrom.String("openssl"), + }, + }, + }, + ExpPC: []*model.PointOfContact{ + { + Subject: p4out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Source", + InPkg: []*model.PkgInputSpec{p1}, + InSrc: []*model.SourceInputSpec{s1, s2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{p1}, + }, + Match: &model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + { + Sub: model.PackageSourceOrArtifactInputs{ + Sources: []*model.SourceInputSpec{s2, s2}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Source: &model.SourceSpec{ + Name: ptrfrom.String("bobsrepo"), + }, + }, + }, + ExpPC: []*model.PointOfContact{ + { + Subject: s2out, + Justification: "test justification", + }, + }, + }, + { + Name: "Query on Artifact", + InSrc: []*model.SourceInputSpec{s1}, + InArt: []*model.ArtifactInputSpec{a1, a2}, + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Artifacts: []*model.ArtifactInputSpec{a1, a2}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + { + Justification: "test justification", + }, + }, + }, + { + Sub: model.PackageSourceOrArtifactInputs{ + Sources: []*model.SourceInputSpec{s1}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + Query: &model.PointOfContactSpec{ + Subject: &model.PackageSourceOrArtifactSpec{ + Artifact: &model.ArtifactSpec{ + Algorithm: ptrfrom.String("sha1"), + }, + }, + }, + ExpPC: []*model.PointOfContact{ + { + Subject: a2out, + Justification: "test justification", + }, + }, + }, + } + ignoreID := cmp.FilterPath(func(p cmp.Path) bool { + return strings.Compare(".ID", p[len(p)-1].String()) == 0 + }, cmp.Ignore()) + ctx := context.Background() + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + b, err := backends.Get("inmem", nil, nil) + if err != nil { + t.Fatalf("Could not instantiate testing backend: %v", err) + } + for _, p := range test.InPkg { + if _, err := b.IngestPackage(ctx, *p); err != nil { + t.Fatalf("Could not ingest package: %v", err) + } + } + for _, s := range test.InSrc { + if _, err := b.IngestSource(ctx, *s); err != nil { + t.Fatalf("Could not ingest source: %v", err) + } + } + for _, a := range test.InArt { + if _, err := b.IngestArtifact(ctx, a); err != nil { + t.Fatalf("Could not ingest artifact: %v", err) + } + } + for _, o := range test.Calls { + _, err := b.IngestPointOfContacts(ctx, o.Sub, o.Match, o.PC) + if (err != nil) != test.ExpIngestErr { + t.Fatalf("did not get expected ingest error, want: %v, got: %v", test.ExpIngestErr, err) + } + if err != nil { + return + } + } + got, err := b.PointOfContact(ctx, test.Query) + if (err != nil) != test.ExpQueryErr { + t.Fatalf("did not get expected query error, want: %v, got: %v", test.ExpQueryErr, err) + } + if err != nil { + return + } + if diff := cmp.Diff(test.ExpPC, got, ignoreID); diff != "" { + t.Errorf("Unexpected results. (-want +got):\n%s", diff) + } + }) + } +} + func TestPointOfContactNeighbors(t *testing.T) { type call struct { Sub model.PackageSourceOrArtifactInput diff --git a/pkg/assembler/backends/neo4j/contact.go b/pkg/assembler/backends/neo4j/contact.go index 4b4ab4b770..bbb7183c3b 100644 --- a/pkg/assembler/backends/neo4j/contact.go +++ b/pkg/assembler/backends/neo4j/contact.go @@ -29,3 +29,7 @@ func (c *neo4jClient) IngestPointOfContact(ctx context.Context, subject model.Pa func (c *neo4jClient) PointOfContact(ctx context.Context, pointOfContactSpec *model.PointOfContactSpec) ([]*model.PointOfContact, error) { return nil, fmt.Errorf("not implemented: PointOfContact") } + +func (c *neo4jClient) IngestPointOfContacts(ctx context.Context, subjects model.PackageSourceOrArtifactInputs, pkgMatchType *model.MatchFlags, pointOfContacts []*model.PointOfContactInputSpec) ([]string, error) { + return nil, fmt.Errorf("not implemented: IngestPointOfContacts") +} diff --git a/pkg/assembler/clients/generated/operations.go b/pkg/assembler/clients/generated/operations.go index 97727342c0..1d696944b7 100644 --- a/pkg/assembler/clients/generated/operations.go +++ b/pkg/assembler/clients/generated/operations.go @@ -20163,6 +20163,17 @@ func (v *PointOfContactArtifactResponse) GetIngestPointOfContact() string { return v.IngestPointOfContact } +// PointOfContactArtifactsResponse is returned by PointOfContactArtifacts on success. +type PointOfContactArtifactsResponse struct { + // Adds bulk PointOfContact attestations to a package, source or artifact. The returned array of IDs can be a an array of empty string. + IngestPointOfContacts []string `json:"ingestPointOfContacts"` +} + +// GetIngestPointOfContacts returns PointOfContactArtifactsResponse.IngestPointOfContacts, and is useful for accessing the field via an interface. +func (v *PointOfContactArtifactsResponse) GetIngestPointOfContacts() []string { + return v.IngestPointOfContacts +} + // PointOfContactInputSpec represents the mutation input to ingest a PointOfContact evidence. type PointOfContactInputSpec struct { Email string `json:"email"` @@ -20200,6 +20211,17 @@ type PointOfContactPkgResponse struct { // GetIngestPointOfContact returns PointOfContactPkgResponse.IngestPointOfContact, and is useful for accessing the field via an interface. func (v *PointOfContactPkgResponse) GetIngestPointOfContact() string { return v.IngestPointOfContact } +// PointOfContactPkgsResponse is returned by PointOfContactPkgs on success. +type PointOfContactPkgsResponse struct { + // Adds bulk PointOfContact attestations to a package, source or artifact. The returned array of IDs can be a an array of empty string. + IngestPointOfContacts []string `json:"ingestPointOfContacts"` +} + +// GetIngestPointOfContacts returns PointOfContactPkgsResponse.IngestPointOfContacts, and is useful for accessing the field via an interface. +func (v *PointOfContactPkgsResponse) GetIngestPointOfContacts() []string { + return v.IngestPointOfContacts +} + // PointOfContactSrcResponse is returned by PointOfContactSrc on success. type PointOfContactSrcResponse struct { // Adds a PointOfContact attestation to a package, source or artifact. The returned ID can be empty string. @@ -20209,6 +20231,17 @@ type PointOfContactSrcResponse struct { // GetIngestPointOfContact returns PointOfContactSrcResponse.IngestPointOfContact, and is useful for accessing the field via an interface. func (v *PointOfContactSrcResponse) GetIngestPointOfContact() string { return v.IngestPointOfContact } +// PointOfContactSrcsResponse is returned by PointOfContactSrcs on success. +type PointOfContactSrcsResponse struct { + // Adds bulk PointOfContact attestations to a package, source or artifact. The returned array of IDs can be a an array of empty string. + IngestPointOfContacts []string `json:"ingestPointOfContacts"` +} + +// GetIngestPointOfContacts returns PointOfContactSrcsResponse.IngestPointOfContacts, and is useful for accessing the field via an interface. +func (v *PointOfContactSrcsResponse) GetIngestPointOfContacts() []string { + return v.IngestPointOfContacts +} + // SLSAForArtifactResponse is returned by SLSAForArtifact on success. type SLSAForArtifactResponse struct { // Ingests a SLSA attestation. The returned ID can be empty string. @@ -21768,6 +21801,20 @@ func (v *__PointOfContactArtifactInput) GetPointOfContact() PointOfContactInputS return v.PointOfContact } +// __PointOfContactArtifactsInput is used internally by genqlient +type __PointOfContactArtifactsInput struct { + Artifacts []ArtifactInputSpec `json:"artifacts"` + PointOfContacts []PointOfContactInputSpec `json:"pointOfContacts"` +} + +// GetArtifacts returns __PointOfContactArtifactsInput.Artifacts, and is useful for accessing the field via an interface. +func (v *__PointOfContactArtifactsInput) GetArtifacts() []ArtifactInputSpec { return v.Artifacts } + +// GetPointOfContacts returns __PointOfContactArtifactsInput.PointOfContacts, and is useful for accessing the field via an interface. +func (v *__PointOfContactArtifactsInput) GetPointOfContacts() []PointOfContactInputSpec { + return v.PointOfContacts +} + // __PointOfContactPkgInput is used internally by genqlient type __PointOfContactPkgInput struct { Pkg PkgInputSpec `json:"pkg"` @@ -21786,6 +21833,24 @@ func (v *__PointOfContactPkgInput) GetPointOfContact() PointOfContactInputSpec { return v.PointOfContact } +// __PointOfContactPkgsInput is used internally by genqlient +type __PointOfContactPkgsInput struct { + Pkgs []PkgInputSpec `json:"pkgs"` + PkgMatchType MatchFlags `json:"pkgMatchType"` + PointOfContacts []PointOfContactInputSpec `json:"pointOfContacts"` +} + +// GetPkgs returns __PointOfContactPkgsInput.Pkgs, and is useful for accessing the field via an interface. +func (v *__PointOfContactPkgsInput) GetPkgs() []PkgInputSpec { return v.Pkgs } + +// GetPkgMatchType returns __PointOfContactPkgsInput.PkgMatchType, and is useful for accessing the field via an interface. +func (v *__PointOfContactPkgsInput) GetPkgMatchType() MatchFlags { return v.PkgMatchType } + +// GetPointOfContacts returns __PointOfContactPkgsInput.PointOfContacts, and is useful for accessing the field via an interface. +func (v *__PointOfContactPkgsInput) GetPointOfContacts() []PointOfContactInputSpec { + return v.PointOfContacts +} + // __PointOfContactSrcInput is used internally by genqlient type __PointOfContactSrcInput struct { Source SourceInputSpec `json:"source"` @@ -21800,6 +21865,20 @@ func (v *__PointOfContactSrcInput) GetPointOfContact() PointOfContactInputSpec { return v.PointOfContact } +// __PointOfContactSrcsInput is used internally by genqlient +type __PointOfContactSrcsInput struct { + Sources []SourceInputSpec `json:"sources"` + PointOfContacts []PointOfContactInputSpec `json:"pointOfContacts"` +} + +// GetSources returns __PointOfContactSrcsInput.Sources, and is useful for accessing the field via an interface. +func (v *__PointOfContactSrcsInput) GetSources() []SourceInputSpec { return v.Sources } + +// GetPointOfContacts returns __PointOfContactSrcsInput.PointOfContacts, and is useful for accessing the field via an interface. +func (v *__PointOfContactSrcsInput) GetPointOfContacts() []PointOfContactInputSpec { + return v.PointOfContacts +} + // __SLSAForArtifactInput is used internally by genqlient type __SLSAForArtifactInput struct { Artifact ArtifactInputSpec `json:"artifact"` @@ -26477,6 +26556,41 @@ func PointOfContactArtifact( return &data, err } +// The query or mutation executed by PointOfContactArtifacts. +const PointOfContactArtifacts_Operation = ` +mutation PointOfContactArtifacts ($artifacts: [ArtifactInputSpec!]!, $pointOfContacts: [PointOfContactInputSpec!]!) { + ingestPointOfContacts(subjects: {artifacts:$artifacts}, pkgMatchType: {pkg:ALL_VERSIONS}, pointOfContacts: $pointOfContacts) +} +` + +func PointOfContactArtifacts( + ctx context.Context, + client graphql.Client, + artifacts []ArtifactInputSpec, + pointOfContacts []PointOfContactInputSpec, +) (*PointOfContactArtifactsResponse, error) { + req := &graphql.Request{ + OpName: "PointOfContactArtifacts", + Query: PointOfContactArtifacts_Operation, + Variables: &__PointOfContactArtifactsInput{ + Artifacts: artifacts, + PointOfContacts: pointOfContacts, + }, + } + var err error + + var data PointOfContactArtifactsResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + // The query or mutation executed by PointOfContactPkg. const PointOfContactPkg_Operation = ` mutation PointOfContactPkg ($pkg: PkgInputSpec!, $pkgMatchType: MatchFlags!, $pointOfContact: PointOfContactInputSpec!) { @@ -26514,6 +26628,43 @@ func PointOfContactPkg( return &data, err } +// The query or mutation executed by PointOfContactPkgs. +const PointOfContactPkgs_Operation = ` +mutation PointOfContactPkgs ($pkgs: [PkgInputSpec!]!, $pkgMatchType: MatchFlags!, $pointOfContacts: [PointOfContactInputSpec!]!) { + ingestPointOfContacts(subjects: {packages:$pkgs}, pkgMatchType: $pkgMatchType, pointOfContacts: $pointOfContacts) +} +` + +func PointOfContactPkgs( + ctx context.Context, + client graphql.Client, + pkgs []PkgInputSpec, + pkgMatchType MatchFlags, + pointOfContacts []PointOfContactInputSpec, +) (*PointOfContactPkgsResponse, error) { + req := &graphql.Request{ + OpName: "PointOfContactPkgs", + Query: PointOfContactPkgs_Operation, + Variables: &__PointOfContactPkgsInput{ + Pkgs: pkgs, + PkgMatchType: pkgMatchType, + PointOfContacts: pointOfContacts, + }, + } + var err error + + var data PointOfContactPkgsResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + // The query or mutation executed by PointOfContactSrc. const PointOfContactSrc_Operation = ` mutation PointOfContactSrc ($source: SourceInputSpec!, $pointOfContact: PointOfContactInputSpec!) { @@ -26549,6 +26700,41 @@ func PointOfContactSrc( return &data, err } +// The query or mutation executed by PointOfContactSrcs. +const PointOfContactSrcs_Operation = ` +mutation PointOfContactSrcs ($sources: [SourceInputSpec!]!, $pointOfContacts: [PointOfContactInputSpec!]!) { + ingestPointOfContacts(subjects: {sources:$sources}, pkgMatchType: {pkg:ALL_VERSIONS}, pointOfContacts: $pointOfContacts) +} +` + +func PointOfContactSrcs( + ctx context.Context, + client graphql.Client, + sources []SourceInputSpec, + pointOfContacts []PointOfContactInputSpec, +) (*PointOfContactSrcsResponse, error) { + req := &graphql.Request{ + OpName: "PointOfContactSrcs", + Query: PointOfContactSrcs_Operation, + Variables: &__PointOfContactSrcsInput{ + Sources: sources, + PointOfContacts: pointOfContacts, + }, + } + var err error + + var data PointOfContactSrcsResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + // The query or mutation executed by SLSAForArtifact. const SLSAForArtifact_Operation = ` mutation SLSAForArtifact ($artifact: ArtifactInputSpec!, $materials: [ArtifactInputSpec!]!, $builder: BuilderInputSpec!, $slsa: SLSAInputSpec!) { diff --git a/pkg/assembler/clients/helpers/assembler.go b/pkg/assembler/clients/helpers/assembler.go index 3d34e46049..77b62d3067 100644 --- a/pkg/assembler/clients/helpers/assembler.go +++ b/pkg/assembler/clients/helpers/assembler.go @@ -76,6 +76,14 @@ func GetAssembler(ctx context.Context, gqlclient graphql.Client) func([]assemble } } + licenses := p.GetLicenses(ctx) + logger.Infof("assembling License: %v", len(licenses)) + for _, v := range licenses { + if err := ingestLicense(ctx, gqlclient, &v); err != nil { + return err + } + } + logger.Infof("assembling CertifyScorecard: %v", len(p.CertifyScorecard)) for _, v := range p.CertifyScorecard { if err := ingestCertifyScorecard(ctx, gqlclient, v); err != nil { @@ -187,6 +195,13 @@ func GetAssembler(ctx context.Context, gqlclient graphql.Client) func([]assemble return err } } + + logger.Infof("assembling CertifyLegal : %v", len(p.CertifyLegal)) + for _, cl := range p.CertifyLegal { + if err := ingestCertifyLegal(ctx, gqlclient, cl); err != nil { + return err + } + } } return nil } @@ -217,6 +232,11 @@ func ingestVulnerability(ctx context.Context, client graphql.Client, v *model.Vu return err } +func ingestLicense(ctx context.Context, client graphql.Client, l *model.LicenseInputSpec) error { + _, err := model.IngestLicense(ctx, client, *l) + return err +} + func ingestCertifyScorecard(ctx context.Context, client graphql.Client, v assembler.CertifyScorecardIngest) error { _, err := model.CertifyScorecard(ctx, client, *v.Source, *v.Scorecard) return err @@ -409,6 +429,22 @@ func ingestHashEqual(ctx context.Context, client graphql.Client, v assembler.Has return err } +func ingestCertifyLegal(ctx context.Context, client graphql.Client, v assembler.CertifyLegalIngest) error { + if v.Pkg != nil && v.Src != nil { + return fmt.Errorf("unable to create CertifyLegal with both Src and Pkg subject specified") + } + if v.Pkg == nil && v.Src == nil { + return fmt.Errorf("unable to create CertifyLegal without either Src and Pkg subject specified") + } + + if v.Src != nil { + _, err := model.CertifyLegalSrc(ctx, client, *v.Src, v.Declared, v.Discovered, *v.CertifyLegal) + return err + } + _, err := model.CertifyLegalPkg(ctx, client, *v.Pkg, v.Declared, v.Discovered, *v.CertifyLegal) + return err +} + func validatePackageSourceOrArtifactInput(pkg *model.PkgInputSpec, src *model.SourceInputSpec, artifact *model.ArtifactInputSpec, path string) error { valuesDefined := 0 if pkg != nil { diff --git a/pkg/assembler/clients/helpers/bulk.go b/pkg/assembler/clients/helpers/bulk.go index 7cf4eedc58..0b5c1aa388 100644 --- a/pkg/assembler/clients/helpers/bulk.go +++ b/pkg/assembler/clients/helpers/bulk.go @@ -92,6 +92,12 @@ func GetBulkAssembler(ctx context.Context, gqlclient graphql.Client) func([]asse return fmt.Errorf("ingestVulnerabilities failed with error: %w", err) } + licenses := p.GetLicenses(ctx) + logger.Infof("assembling Licenses: %v", len(licenses)) + if err := ingestLicenses(ctx, gqlclient, licenses); err != nil { + return fmt.Errorf("ingestLicenses failed with error: %w", err) + } + logger.Infof("assembling CertifyScorecard: %v", len(p.CertifyScorecard)) if err := ingestCertifyScorecards(ctx, gqlclient, p.CertifyScorecard); err != nil { return fmt.Errorf("ingestCertifyScorecards failed with error: %w", err) @@ -149,13 +155,9 @@ func GetBulkAssembler(ctx context.Context, gqlclient graphql.Client) func([]asse } - // TODO: add bulk ingestion for PointOfContact logger.Infof("assembling PointOfContact: %v", len(p.PointOfContact)) - for _, poc := range p.PointOfContact { - if err := ingestPointOfContact(ctx, gqlclient, poc); err != nil { - return fmt.Errorf("ingestPointOfContact failed with error: %w", err) - - } + if err := ingestPointOfContacts(ctx, gqlclient, p.PointOfContact); err != nil { + return fmt.Errorf("ingestPointOfContacts failed with error: %w", err) } logger.Infof("assembling HasMetadata: %v", len(p.HasMetadata)) @@ -182,6 +184,11 @@ func GetBulkAssembler(ctx context.Context, gqlclient graphql.Client) func([]asse if err := ingestPkgEquals(ctx, gqlclient, p.PkgEqual); err != nil { return fmt.Errorf("ingestPkgEquals failed with error: %w", err) } + + logger.Infof("assembling CertifyLegal : %v", len(p.CertifyLegal)) + if err := ingestCertifyLegals(ctx, gqlclient, p.CertifyLegal); err != nil { + return fmt.Errorf("ingestCertifyLegals failed with error: %w", err) + } } return nil } @@ -227,6 +234,14 @@ func ingestVulnerabilities(ctx context.Context, client graphql.Client, v []model return nil } +func ingestLicenses(ctx context.Context, client graphql.Client, v []model.LicenseInputSpec) error { + _, err := model.IngestLicenses(ctx, client, v) + if err != nil { + return fmt.Errorf("ingestLicenses failed with error: %w", err) + } + return nil +} + func ingestCertifyVulns(ctx context.Context, client graphql.Client, cv []assembler.CertifyVulnIngest) error { var pkgs []model.PkgInputSpec var vulnerabilities []model.VulnerabilityInputSpec @@ -468,6 +483,62 @@ func ingestHasSBOMs(ctx context.Context, client graphql.Client, v []assembler.Ha return nil } +func ingestPointOfContacts(ctx context.Context, client graphql.Client, poc []assembler.PointOfContactIngest) error { + var pkgVersions []model.PkgInputSpec + var pkgNames []model.PkgInputSpec + var sources []model.SourceInputSpec + var artifacts []model.ArtifactInputSpec + var pkgVersionPOC []model.PointOfContactInputSpec + var pkgNamePOC []model.PointOfContactInputSpec + var srcPOC []model.PointOfContactInputSpec + var artPOC []model.PointOfContactInputSpec + for _, ingest := range poc { + if err := validatePackageSourceOrArtifactInput(ingest.Pkg, ingest.Src, ingest.Artifact, "ingestPointOfContacts"); err != nil { + return fmt.Errorf("input validation failed for ingestPointOfContacts: %w", err) + } + if ingest.Pkg != nil { + if ingest.PkgMatchFlag.Pkg == model.PkgMatchTypeSpecificVersion { + pkgVersions = append(pkgVersions, *ingest.Pkg) + pkgVersionPOC = append(pkgVersionPOC, *ingest.PointOfContact) + } else { + pkgNames = append(pkgNames, *ingest.Pkg) + pkgNamePOC = append(pkgNamePOC, *ingest.PointOfContact) + } + } else if ingest.Src != nil { + sources = append(sources, *ingest.Src) + srcPOC = append(srcPOC, *ingest.PointOfContact) + } else { + artifacts = append(artifacts, *ingest.Artifact) + artPOC = append(artPOC, *ingest.PointOfContact) + } + } + if len(pkgVersions) > 0 { + _, err := model.PointOfContactPkgs(ctx, client, pkgVersions, model.MatchFlags{Pkg: model.PkgMatchTypeSpecificVersion}, pkgVersionPOC) + if err != nil { + return fmt.Errorf("HasMetadataPkgs - specific version failed with error: %w", err) + } + } + if len(pkgNames) > 0 { + _, err := model.PointOfContactPkgs(ctx, client, pkgNames, model.MatchFlags{Pkg: model.PkgMatchTypeAllVersions}, pkgNamePOC) + if err != nil { + return fmt.Errorf("HasMetadataPkgs - all versions failed with error: %w", err) + } + } + if len(sources) > 0 { + _, err := model.PointOfContactSrcs(ctx, client, sources, srcPOC) + if err != nil { + return fmt.Errorf("HasMetadataSrcs failed with error: %w", err) + } + } + if len(artifacts) > 0 { + _, err := model.PointOfContactArtifacts(ctx, client, artifacts, artPOC) + if err != nil { + return fmt.Errorf("HasMetadataArtifacts failed with error: %w", err) + } + } + return nil +} + func ingestBulkHasMetadata(ctx context.Context, client graphql.Client, v []assembler.HasMetadataIngest) error { var pkgVersions []model.PkgInputSpec var pkgNames []model.PkgInputSpec @@ -676,3 +747,48 @@ func ingestIsOccurrences(ctx context.Context, client graphql.Client, v []assembl } return nil } + +func ingestCertifyLegals(ctx context.Context, client graphql.Client, v []assembler.CertifyLegalIngest) error { + var pkgs []model.PkgInputSpec + var sources []model.SourceInputSpec + var pkgDec [][]model.LicenseInputSpec + var pkgDis [][]model.LicenseInputSpec + var pkgCL []model.CertifyLegalInputSpec + var srcDec [][]model.LicenseInputSpec + var srcDis [][]model.LicenseInputSpec + var srcCL []model.CertifyLegalInputSpec + for _, ingest := range v { + + if ingest.Pkg != nil && ingest.Src != nil { + return fmt.Errorf("unable to create CertifyLegal with both Src and Pkg subject specified") + } + if ingest.Pkg == nil && ingest.Src == nil { + return fmt.Errorf("unable to create CertifyLegal without either Src and Pkg subject specified") + } + + if ingest.Pkg != nil { + pkgs = append(pkgs, *ingest.Pkg) + pkgDec = append(pkgDec, ingest.Declared) + pkgDis = append(pkgDis, ingest.Discovered) + pkgCL = append(pkgCL, *ingest.CertifyLegal) + } else { + sources = append(sources, *ingest.Src) + srcDec = append(srcDec, ingest.Declared) + srcDis = append(srcDis, ingest.Discovered) + srcCL = append(srcCL, *ingest.CertifyLegal) + } + } + if len(sources) > 0 { + _, err := model.CertifyLegalSrcs(ctx, client, sources, srcDec, srcDis, srcCL) + if err != nil { + return fmt.Errorf("certifyLegalSrc failed with error: %w", err) + } + } + if len(pkgs) > 0 { + _, err := model.CertifyLegalPkgs(ctx, client, pkgs, pkgDec, pkgDis, pkgCL) + if err != nil { + return fmt.Errorf("certifyLegalPkg failed with error: %w", err) + } + } + return nil +} diff --git a/pkg/assembler/clients/operations/contact.graphql b/pkg/assembler/clients/operations/contact.graphql index 1432fc92b7..492d10ebd2 100644 --- a/pkg/assembler/clients/operations/contact.graphql +++ b/pkg/assembler/clients/operations/contact.graphql @@ -50,3 +50,39 @@ mutation PointOfContactArtifact( pointOfContact: $pointOfContact ) } + +# Defines the GraphQL operations to bulk ingest a PointOfContact into GUAC + +mutation PointOfContactPkgs( + $pkgs: [PkgInputSpec!]! + $pkgMatchType: MatchFlags! + $pointOfContacts: [PointOfContactInputSpec!]! +) { + ingestPointOfContacts( + subjects: { packages: $pkgs } + pkgMatchType: $pkgMatchType + pointOfContacts: $pointOfContacts + ) +} + +mutation PointOfContactSrcs( + $sources: [SourceInputSpec!]! + $pointOfContacts: [PointOfContactInputSpec!]! +) { + ingestPointOfContacts( + subjects: { sources: $sources } + pkgMatchType: { pkg: ALL_VERSIONS } + pointOfContacts: $pointOfContacts + ) +} + +mutation PointOfContactArtifacts( + $artifacts: [ArtifactInputSpec!]! + $pointOfContacts: [PointOfContactInputSpec!]! +) { + ingestPointOfContacts( + subjects: { artifacts: $artifacts } + pkgMatchType: { pkg: ALL_VERSIONS } + pointOfContacts: $pointOfContacts + ) +} diff --git a/pkg/assembler/clients/operations/metadata.graphql b/pkg/assembler/clients/operations/metadata.graphql index 4c6531aff6..abcefdc236 100644 --- a/pkg/assembler/clients/operations/metadata.graphql +++ b/pkg/assembler/clients/operations/metadata.graphql @@ -51,7 +51,6 @@ mutation HasMetadataArtifact( ) } - # Defines the GraphQL operations to bulk ingest a HasMetadata into GUAC mutation HasMetadataPkgs( diff --git a/pkg/assembler/graphql/generated/artifact.generated.go b/pkg/assembler/graphql/generated/artifact.generated.go index 49015d39c4..e9b3ca9f3f 100644 --- a/pkg/assembler/graphql/generated/artifact.generated.go +++ b/pkg/assembler/graphql/generated/artifact.generated.go @@ -36,6 +36,7 @@ type MutationResolver interface { IngestCertifyVuln(ctx context.Context, pkg model.PkgInputSpec, vulnerability model.VulnerabilityInputSpec, certifyVuln model.ScanMetadataInput) (string, error) IngestCertifyVulns(ctx context.Context, pkgs []*model.PkgInputSpec, vulnerabilities []*model.VulnerabilityInputSpec, certifyVulns []*model.ScanMetadataInput) ([]string, error) IngestPointOfContact(ctx context.Context, subject model.PackageSourceOrArtifactInput, pkgMatchType model.MatchFlags, pointOfContact model.PointOfContactInputSpec) (string, error) + IngestPointOfContacts(ctx context.Context, subjects model.PackageSourceOrArtifactInputs, pkgMatchType model.MatchFlags, pointOfContacts []*model.PointOfContactInputSpec) ([]string, error) IngestHasSbom(ctx context.Context, subject model.PackageOrArtifactInput, hasSbom model.HasSBOMInputSpec) (string, error) IngestHasSBOMs(ctx context.Context, subjects model.PackageOrArtifactInputs, hasSBOMs []*model.HasSBOMInputSpec) ([]string, error) IngestSlsa(ctx context.Context, subject model.ArtifactInputSpec, builtFrom []*model.ArtifactInputSpec, builtBy model.BuilderInputSpec, slsa model.SLSAInputSpec) (string, error) @@ -996,6 +997,39 @@ func (ec *executionContext) field_Mutation_ingestPointOfContact_args(ctx context return args, nil } +func (ec *executionContext) field_Mutation_ingestPointOfContacts_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.PackageSourceOrArtifactInputs + if tmp, ok := rawArgs["subjects"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("subjects")) + arg0, err = ec.unmarshalNPackageSourceOrArtifactInputs2githubᚗcomᚋguacsecᚋguacᚋpkgᚋassemblerᚋgraphqlᚋmodelᚐPackageSourceOrArtifactInputs(ctx, tmp) + if err != nil { + return nil, err + } + } + args["subjects"] = arg0 + var arg1 model.MatchFlags + if tmp, ok := rawArgs["pkgMatchType"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pkgMatchType")) + arg1, err = ec.unmarshalNMatchFlags2githubᚗcomᚋguacsecᚋguacᚋpkgᚋassemblerᚋgraphqlᚋmodelᚐMatchFlags(ctx, tmp) + if err != nil { + return nil, err + } + } + args["pkgMatchType"] = arg1 + var arg2 []*model.PointOfContactInputSpec + if tmp, ok := rawArgs["pointOfContacts"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pointOfContacts")) + arg2, err = ec.unmarshalNPointOfContactInputSpec2ᚕᚖgithubᚗcomᚋguacsecᚋguacᚋpkgᚋassemblerᚋgraphqlᚋmodelᚐPointOfContactInputSpecᚄ(ctx, tmp) + if err != nil { + return nil, err + } + } + args["pointOfContacts"] = arg2 + return args, nil +} + func (ec *executionContext) field_Mutation_ingestSLSA_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -2890,6 +2924,61 @@ func (ec *executionContext) fieldContext_Mutation_ingestPointOfContact(ctx conte return fc, nil } +func (ec *executionContext) _Mutation_ingestPointOfContacts(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_ingestPointOfContacts(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().IngestPointOfContacts(rctx, fc.Args["subjects"].(model.PackageSourceOrArtifactInputs), fc.Args["pkgMatchType"].(model.MatchFlags), fc.Args["pointOfContacts"].([]*model.PointOfContactInputSpec)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]string) + fc.Result = res + return ec.marshalNID2ᚕstringᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_ingestPointOfContacts(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_ingestPointOfContacts_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_ingestHasSBOM(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_ingestHasSBOM(ctx, field) if err != nil { @@ -6626,6 +6715,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "ingestPointOfContacts": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_ingestPointOfContacts(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "ingestHasSBOM": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_ingestHasSBOM(ctx, field) diff --git a/pkg/assembler/graphql/generated/contact.generated.go b/pkg/assembler/graphql/generated/contact.generated.go index c73141c8a9..8a080acfc2 100644 --- a/pkg/assembler/graphql/generated/contact.generated.go +++ b/pkg/assembler/graphql/generated/contact.generated.go @@ -696,6 +696,28 @@ func (ec *executionContext) unmarshalNPointOfContactInputSpec2githubᚗcomᚋgua return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNPointOfContactInputSpec2ᚕᚖgithubᚗcomᚋguacsecᚋguacᚋpkgᚋassemblerᚋgraphqlᚋmodelᚐPointOfContactInputSpecᚄ(ctx context.Context, v interface{}) ([]*model.PointOfContactInputSpec, error) { + var vSlice []interface{} + if v != nil { + vSlice = graphql.CoerceList(v) + } + var err error + res := make([]*model.PointOfContactInputSpec, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNPointOfContactInputSpec2ᚖgithubᚗcomᚋguacsecᚋguacᚋpkgᚋassemblerᚋgraphqlᚋmodelᚐPointOfContactInputSpec(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) unmarshalNPointOfContactInputSpec2ᚖgithubᚗcomᚋguacsecᚋguacᚋpkgᚋassemblerᚋgraphqlᚋmodelᚐPointOfContactInputSpec(ctx context.Context, v interface{}) (*model.PointOfContactInputSpec, error) { + res, err := ec.unmarshalInputPointOfContactInputSpec(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNPointOfContactSpec2githubᚗcomᚋguacsecᚋguacᚋpkgᚋassemblerᚋgraphqlᚋmodelᚐPointOfContactSpec(ctx context.Context, v interface{}) (model.PointOfContactSpec, error) { res, err := ec.unmarshalInputPointOfContactSpec(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/assembler/graphql/generated/root_.generated.go b/pkg/assembler/graphql/generated/root_.generated.go index 08c18405a2..8bd6e9888c 100644 --- a/pkg/assembler/graphql/generated/root_.generated.go +++ b/pkg/assembler/graphql/generated/root_.generated.go @@ -211,6 +211,7 @@ type ComplexityRoot struct { IngestPkgEqual func(childComplexity int, pkg model.PkgInputSpec, otherPackage model.PkgInputSpec, pkgEqual model.PkgEqualInputSpec) int IngestPkgEquals func(childComplexity int, pkgs []*model.PkgInputSpec, otherPackages []*model.PkgInputSpec, pkgEquals []*model.PkgEqualInputSpec) int IngestPointOfContact func(childComplexity int, subject model.PackageSourceOrArtifactInput, pkgMatchType model.MatchFlags, pointOfContact model.PointOfContactInputSpec) int + IngestPointOfContacts func(childComplexity int, subjects model.PackageSourceOrArtifactInputs, pkgMatchType model.MatchFlags, pointOfContacts []*model.PointOfContactInputSpec) int IngestSLSAs func(childComplexity int, subjects []*model.ArtifactInputSpec, builtFromList [][]*model.ArtifactInputSpec, builtByList []*model.BuilderInputSpec, slsaList []*model.SLSAInputSpec) int IngestScorecard func(childComplexity int, source model.SourceInputSpec, scorecard model.ScorecardInputSpec) int IngestScorecards func(childComplexity int, sources []*model.SourceInputSpec, scorecards []*model.ScorecardInputSpec) int @@ -1428,6 +1429,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.IngestPointOfContact(childComplexity, args["subject"].(model.PackageSourceOrArtifactInput), args["pkgMatchType"].(model.MatchFlags), args["pointOfContact"].(model.PointOfContactInputSpec)), true + case "Mutation.ingestPointOfContacts": + if e.complexity.Mutation.IngestPointOfContacts == nil { + break + } + + args, err := ec.field_Mutation_ingestPointOfContacts_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.IngestPointOfContacts(childComplexity, args["subjects"].(model.PackageSourceOrArtifactInputs), args["pkgMatchType"].(model.MatchFlags), args["pointOfContacts"].([]*model.PointOfContactInputSpec)), true + case "Mutation.ingestSLSAs": if e.complexity.Mutation.IngestSLSAs == nil { break @@ -3617,6 +3630,12 @@ extend type Mutation { pkgMatchType: MatchFlags! pointOfContact: PointOfContactInputSpec! ): ID! + "Adds bulk PointOfContact attestations to a package, source or artifact. The returned array of IDs can be a an array of empty string." + ingestPointOfContacts( + subjects: PackageSourceOrArtifactInputs! + pkgMatchType: MatchFlags! + pointOfContacts: [PointOfContactInputSpec!]! + ): [ID!]! } `, BuiltIn: false}, {Name: "../schema/hasSBOM.graphql", Input: `# diff --git a/pkg/assembler/graphql/resolvers/contact.resolvers.go b/pkg/assembler/graphql/resolvers/contact.resolvers.go index 8c251ef125..a630b65793 100644 --- a/pkg/assembler/graphql/resolvers/contact.resolvers.go +++ b/pkg/assembler/graphql/resolvers/contact.resolvers.go @@ -25,6 +25,34 @@ func (r *mutationResolver) IngestPointOfContact(ctx context.Context, subject mod return ingestedPOC.ID, err } +// IngestPointOfContacts is the resolver for the ingestPointOfContacts field. +func (r *mutationResolver) IngestPointOfContacts(ctx context.Context, subjects model.PackageSourceOrArtifactInputs, pkgMatchType model.MatchFlags, pointOfContacts []*model.PointOfContactInputSpec) ([]string, error) { + funcName := "IngestPointOfContacts" + valuesDefined := 0 + if len(subjects.Packages) > 0 { + if len(subjects.Packages) != len(pointOfContacts) { + return []string{}, gqlerror.Errorf("%v :: uneven packages and pointOfContacts for ingestion", funcName) + } + valuesDefined = valuesDefined + 1 + } + if len(subjects.Artifacts) > 0 { + if len(subjects.Artifacts) != len(pointOfContacts) { + return []string{}, gqlerror.Errorf("%v :: uneven artifacts and pointOfContacts for ingestion", funcName) + } + valuesDefined = valuesDefined + 1 + } + if len(subjects.Sources) > 0 { + if len(subjects.Sources) != len(pointOfContacts) { + return []string{}, gqlerror.Errorf("%v :: uneven sources and pointOfContacts for ingestion", funcName) + } + valuesDefined = valuesDefined + 1 + } + if valuesDefined != 1 { + return []string{}, gqlerror.Errorf("%v :: must specify at most packages, artifacts or sources", funcName) + } + return r.Backend.IngestPointOfContacts(ctx, subjects, &pkgMatchType, pointOfContacts) +} + // PointOfContact is the resolver for the PointOfContact field. func (r *queryResolver) PointOfContact(ctx context.Context, pointOfContactSpec model.PointOfContactSpec) ([]*model.PointOfContact, error) { if err := helper.ValidatePackageSourceOrArtifactQueryFilter(pointOfContactSpec.Subject); err != nil { diff --git a/pkg/assembler/graphql/resolvers/contact.resolvers_test.go b/pkg/assembler/graphql/resolvers/contact.resolvers_test.go index d4274c26a1..e1d9608b93 100644 --- a/pkg/assembler/graphql/resolvers/contact.resolvers_test.go +++ b/pkg/assembler/graphql/resolvers/contact.resolvers_test.go @@ -97,6 +97,133 @@ func TestIngestPointOfContact(t *testing.T) { } } +func TestIngestPointOfContacts(t *testing.T) { + type call struct { + Sub model.PackageSourceOrArtifactInputs + Match model.MatchFlags + PC []*model.PointOfContactInputSpec + } + tests := []struct { + Name string + Calls []call + ExpIngestErr bool + }{ + { + Name: "Ingest with two packages and one pointOfContact", + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{testdata.P1, testdata.P2}, + }, + Match: model.MatchFlags{ + Pkg: model.PkgMatchTypeSpecificVersion, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + ExpIngestErr: true, + }, + { + Name: "Ingest with two sources and one pointOfContact", + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Sources: []*model.SourceInputSpec{testdata.S1, testdata.S2}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + ExpIngestErr: true, + }, + { + Name: "Ingest with two artifacts and one pointOfContact", + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Artifacts: []*model.ArtifactInputSpec{testdata.A1, testdata.A2}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + ExpIngestErr: true, + }, + { + Name: "Ingest with one package, one source, one artifact and one pointOfContact", + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{testdata.P1}, + Sources: []*model.SourceInputSpec{testdata.S1}, + Artifacts: []*model.ArtifactInputSpec{testdata.A1}, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + ExpIngestErr: true, + }, + { + Name: "HappyPath All Version", + Calls: []call{ + { + Sub: model.PackageSourceOrArtifactInputs{ + Packages: []*model.PkgInputSpec{testdata.P1}, + }, + Match: model.MatchFlags{ + Pkg: model.PkgMatchTypeAllVersions, + }, + PC: []*model.PointOfContactInputSpec{ + { + Justification: "test justification", + }, + }, + }, + }, + }, + } + ctx := context.Background() + ctrl := gomock.NewController(t) + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + b := mocks.NewMockBackend(ctrl) + r := resolvers.Resolver{Backend: b} + for _, o := range test.Calls { + times := 1 + if test.ExpIngestErr { + times = 0 + } + b. + EXPECT(). + IngestPointOfContacts(ctx, o.Sub, &o.Match, o.PC). + Return([]string{}, nil). + Times(times) + _, err := r.Mutation().IngestPointOfContacts(ctx, o.Sub, o.Match, o.PC) + if (err != nil) != test.ExpIngestErr { + t.Fatalf("did not get expected ingest error, want: %v, got: %v", test.ExpIngestErr, err) + } + if err != nil { + return + } + } + }) + } +} + func TestPointOfContact(t *testing.T) { tests := []struct { Name string diff --git a/pkg/assembler/graphql/schema/contact.graphql b/pkg/assembler/graphql/schema/contact.graphql index 96dde055d9..1a10f79aa5 100644 --- a/pkg/assembler/graphql/schema/contact.graphql +++ b/pkg/assembler/graphql/schema/contact.graphql @@ -97,4 +97,10 @@ extend type Mutation { pkgMatchType: MatchFlags! pointOfContact: PointOfContactInputSpec! ): ID! + "Adds bulk PointOfContact attestations to a package, source or artifact. The returned array of IDs can be a an array of empty string." + ingestPointOfContacts( + subjects: PackageSourceOrArtifactInputs! + pkgMatchType: MatchFlags! + pointOfContacts: [PointOfContactInputSpec!]! + ): [ID!]! } diff --git a/pkg/ingestor/parser/common/license.go b/pkg/ingestor/parser/common/license.go new file mode 100644 index 0000000000..b63f80b121 --- /dev/null +++ b/pkg/ingestor/parser/common/license.go @@ -0,0 +1,95 @@ +// +// Copyright 2023 The GUAC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common + +import ( + "fmt" + "hash/fnv" + "strings" + + "github.com/guacsec/guac/pkg/assembler/clients/generated" + "golang.org/x/exp/slices" +) + +var ignore = []string{ + "AND", + "OR", + "WITH", +} + +// Could add exceptions to ignore list, as they are not licenses: +// "389-exception", +// "Autoconf-exception-2.0", +// "Autoconf-exception-3.0", +// "Bison-exception-2.2", +// "Bootloader-exception", +// "Classpath-exception-2.0", +// "CLISP-exception-2.0", +// "DigiRule-FOSS-exception", +// "eCos-exception-2.0", +// "Fawkes-Runtime-exception", +// "FLTK-exception", +// "Font-exception-2.0", +// "freertos-exception-2.0", +// "GCC-exception-2.0", +// "GCC-exception-3.1", +// "gnu-javamail-exception", +// "GPL-3.0-linking-exception", +// "GPL-3.0-linking-source-exception", +// "GPL-CC-1.0", +// "i2p-gpl-java-exception", +// "Libtool-exception", +// "Linux-syscall-note", +// "LLVM-exception", +// "LZMA-exception", +// "mif-exception", +// "OCaml-LGPL-linking-exception", +// "OCCT-exception-1.0", +// "OpenJDK-assembly-exception-1.0", +// "openvpn-openssl-exception", +// "PS-or-PDF-font-exception-20170817", +// "Qt-GPL-exception-1.0", +// "Qt-LGPL-exception-1.1", +// "Qwt-exception-1.0", +// "Swift-exception", +// "u-boot-exception-2.0", +// "Universal-FOSS-exception-1.0", +// "WxWindows-exception-3.1", + +func ParseLicenses(exp string, lv string) []generated.LicenseInputSpec { + if exp == "" { + return nil + } + var rv []generated.LicenseInputSpec + for _, part := range strings.Split(exp, " ") { + p := strings.Trim(part, "()+") + if slices.Contains(ignore, p) { + continue + } + rv = append(rv, generated.LicenseInputSpec{ + Name: p, + ListVersion: &lv, + }) + } + return rv +} + +func HashLicense(inline string) string { + h := fnv.New32a() + h.Write([]byte(inline)) + s := h.Sum32() + return fmt.Sprintf("LicenseRef-%x", s) +} diff --git a/pkg/ingestor/parser/spdx/parse_spdx.go b/pkg/ingestor/parser/spdx/parse_spdx.go index a60a6e19a6..dddc412ee0 100644 --- a/pkg/ingestor/parser/spdx/parse_spdx.go +++ b/pkg/ingestor/parser/spdx/parse_spdx.go @@ -20,8 +20,10 @@ import ( "context" "fmt" "strings" + "time" "github.com/guacsec/guac/pkg/assembler" + "github.com/guacsec/guac/pkg/assembler/clients/generated" model "github.com/guacsec/guac/pkg/assembler/clients/generated" asmhelpers "github.com/guacsec/guac/pkg/assembler/helpers" "github.com/guacsec/guac/pkg/handler/processor" @@ -38,18 +40,21 @@ type spdxParser struct { doc *processor.Document packagePackages map[string][]*model.PkgInputSpec packageArtifacts map[string][]*model.ArtifactInputSpec + packageLegals map[string][]*model.CertifyLegalInputSpec filePackages map[string][]*model.PkgInputSpec fileArtifacts map[string][]*model.ArtifactInputSpec topLevelPackages map[string][]*model.PkgInputSpec identifierStrings *common.IdentifierStrings spdxDoc *spdx.Document topLevelIsHeuristic bool + timeScanned time.Time } func NewSpdxParser() common.DocumentParser { return &spdxParser{ packagePackages: map[string][]*model.PkgInputSpec{}, packageArtifacts: map[string][]*model.ArtifactInputSpec{}, + packageLegals: map[string][]*model.CertifyLegalInputSpec{}, filePackages: map[string][]*model.PkgInputSpec{}, fileArtifacts: map[string][]*model.ArtifactInputSpec{}, topLevelPackages: map[string][]*model.PkgInputSpec{}, @@ -65,6 +70,14 @@ func (s *spdxParser) Parse(ctx context.Context, doc *processor.Document) error { return fmt.Errorf("failed to parse SPDX document: %w", err) } s.spdxDoc = spdxDoc + if spdxDoc.CreationInfo == nil { + return fmt.Errorf("SPDC documentd missing required \"creationInfo\" section.") + } + time, err := time.Parse(time.RFC3339, spdxDoc.CreationInfo.Created) + if err != nil { + return fmt.Errorf("SPDX document had invalid created time %q : %w", spdxDoc.CreationInfo.Created, err) + } + s.timeScanned = time if err := s.getPackages(); err != nil { return err } @@ -135,6 +148,23 @@ func (s *spdxParser) getPackages() error { s.packageArtifacts[string(pac.PackageSPDXIdentifier)] = append(s.packageArtifacts[string(pac.PackageSPDXIdentifier)], artifact) } + if pac.PackageLicenseDeclared != "" || + pac.PackageLicenseConcluded != "" || + pac.PackageCopyrightText != "" { + cl := &model.CertifyLegalInputSpec{ + DeclaredLicense: pac.PackageLicenseDeclared, + DiscoveredLicense: pac.PackageLicenseConcluded, + Attribution: pac.PackageCopyrightText, + Justification: "Found in SPDX document.", + TimeScanned: s.timeScanned, + } + if pac.PackageLicenseComments != "" { + cl.Justification = fmt.Sprintf("%s : %s", cl.Justification, pac.PackageLicenseComments) + } + s.packageLegals[string(pac.PackageSPDXIdentifier)] = append( + s.packageLegals[string(pac.PackageSPDXIdentifier)], cl) + } + } // If there is no top level Spdx Id that can be derived from the relationships, we take a best guess for the SpdxId. @@ -296,9 +326,63 @@ func (s *spdxParser) GetPredicates(ctx context.Context) *assembler.IngestPredica } } + for id, cls := range s.packageLegals { + for _, cl := range cls { + dec := common.ParseLicenses(cl.DeclaredLicense, s.spdxDoc.CreationInfo.LicenseListVersion) + dis := common.ParseLicenses(cl.DiscoveredLicense, s.spdxDoc.CreationInfo.LicenseListVersion) + for i := range dec { + o, n := fixLicense(ctx, &dec[i], s.spdxDoc.OtherLicenses) + if o != "" { + exp := strings.ReplaceAll(cl.DeclaredLicense, o, n) + cl.DeclaredLicense = exp + } + } + for i := range dis { + o, n := fixLicense(ctx, &dis[i], s.spdxDoc.OtherLicenses) + if o != "" { + exp := strings.ReplaceAll(cl.DiscoveredLicense, o, n) + cl.DiscoveredLicense = exp + } + } + for _, pkg := range s.packagePackages[id] { + cli := assembler.CertifyLegalIngest{ + Pkg: pkg, + Declared: dec, + Discovered: dis, + CertifyLegal: cl, + } + preds.CertifyLegal = append(preds.CertifyLegal, cli) + } + } + } + return preds } +func fixLicense(ctx context.Context, l *generated.LicenseInputSpec, ol []*spdx.OtherLicense) (string, string) { + logger := logging.FromContext(ctx) + if !strings.HasPrefix(l.Name, "LicenseRef-") { + return "", "" + } + oldName := l.Name + l.ListVersion = nil + found := false + for _, o := range ol { + if o.LicenseIdentifier == l.Name { + l.Inline = &o.ExtractedText + found = true + break + } + } + if !found { + logger.Error("License identifier %q not found in OtherLicenses", l.Name) + s := "Not found" + l.Inline = &s + } + l.Name = common.HashLicense(*l.Inline) + return oldName, l.Name +} + func isDependency(rel string) bool { return map[string]bool{ spdx_common.TypeRelationshipContains: true, diff --git a/pkg/ingestor/parser/spdx/parse_spdx_test.go b/pkg/ingestor/parser/spdx/parse_spdx_test.go index 98949fad9b..de4c2fb0dc 100644 --- a/pkg/ingestor/parser/spdx/parse_spdx_test.go +++ b/pkg/ingestor/parser/spdx/parse_spdx_test.go @@ -18,9 +18,11 @@ package spdx import ( "context" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/guacsec/guac/internal/testing/ptrfrom" "github.com/guacsec/guac/internal/testing/testdata" "github.com/guacsec/guac/pkg/assembler" "github.com/guacsec/guac/pkg/assembler/clients/generated" @@ -42,20 +44,21 @@ func Test_spdxParser(t *testing.T) { doc *processor.Document wantPredicates *assembler.IngestPredicates wantErr bool - }{{ - name: "valid big SPDX document", - doc: &processor.Document{ - Blob: testdata.SpdxExampleAlpine, - Format: processor.FormatJSON, - Type: processor.DocumentSPDX, - SourceInformation: processor.SourceInformation{ - Collector: "TestCollector", - Source: "TestSource", + }{ + { + name: "valid big SPDX document", + doc: &processor.Document{ + Blob: testdata.SpdxExampleAlpine, + Format: processor.FormatJSON, + Type: processor.DocumentSPDX, + SourceInformation: processor.SourceInformation{ + Collector: "TestCollector", + Source: "TestSource", + }, }, + wantPredicates: &testdata.SpdxIngestionPredicates, + wantErr: false, }, - wantPredicates: &testdata.SpdxIngestionPredicates, - wantErr: false, - }, { name: "SPDX with DESCRIBES relationship populates pUrl from described element", additionalOpts: []cmp.Option{ @@ -67,6 +70,7 @@ func Test_spdxParser(t *testing.T) { "spdxVersion": "SPDX-2.3", "SPDXID":"SPDXRef-DOCUMENT", "name":"sbom-sha256:a743268cd3c56f921f3fb706cc0425c8ab78119fd433e38bb7c5dcd5635b0d10", + "creationInfo": { "created": "2023-01-01T01:01:01.00Z" }, "packages":[ { "SPDXID":"SPDXRef-Package-sha256-a743268cd3c56f921f3fb706cc0425c8ab78119fd433e38bb7c5dcd5635b0d10", @@ -114,6 +118,7 @@ func Test_spdxParser(t *testing.T) { "spdxVersion": "SPDX-2.3", "SPDXID":"SPDXRef-DOCUMENT", "name":"sbom-sha256:a743268cd3c56f921f3fb706cc0425c8ab78119fd433e38bb7c5dcd5635b0d10", + "creationInfo": { "created": "2022-09-24T17:27:55.556104Z" }, "packages":[ { "SPDXID":"SPDXRef-Package-sha256-a743268cd3c56f921f3fb706cc0425c8ab78119fd433e38bb7c5dcd5635b0d10", @@ -178,6 +183,7 @@ func Test_spdxParser(t *testing.T) { "spdxVersion": "SPDX-2.3", "SPDXID":"SPDXRef-DOCUMENT", "name":"sbom-sha256:a743268cd3c56f921f3fb706cc0425c8ab78119fd433e38bb7c5dcd5635b0d10", + "creationInfo": { "created": "2022-09-24T17:27:55.556104Z" }, "packages":[ { "SPDXID":"SPDXRef-Package-sha256-a743268cd3c56f921f3fb706cc0425c8ab78119fd433e38bb7c5dcd5635b0d10", @@ -225,6 +231,7 @@ func Test_spdxParser(t *testing.T) { "spdxVersion": "SPDX-2.3", "SPDXID":"SPDXRef-DOCUMENT", "name":"sbom-sha256:a743268cd3c56f921f3fb706cc0425c8ab78119fd433e38bb7c5dcd5635b0d10", + "creationInfo": { "created": "2022-09-24T17:27:55.556104Z" }, "relationships":[ { "spdxElementId":"SPDXRef-Package-sha256-a743268cd3c56f921f3fb706cc0425c8ab78119fd433e38bb7c5dcd5635b0d10", @@ -262,6 +269,7 @@ func Test_spdxParser(t *testing.T) { "SPDXRef-6dcd47a4-bfcb-47d7-8ee4-60b6dc4861a8" ], "name":"test-sbom", + "creationInfo": { "created": "2022-09-24T17:27:55.556104Z" }, "packages":[ { "SPDXID": "SPDXRef-8c5bc68a-d747-48de-b737-bc9703c330e7", @@ -339,6 +347,7 @@ func Test_spdxParser(t *testing.T) { "SPDXID":"SPDXRef-DOCUMENT", "spdxVersion": "SPDX-2.2", "name":"testsbom", + "creationInfo": { "created": "2022-09-24T17:27:55.556104Z" }, "files":[ { "fileName": "file1", @@ -436,6 +445,7 @@ func Test_spdxParser(t *testing.T) { "SPDXID":"SPDXRef-DOCUMENT", "spdxVersion": "SPDX-2.2", "name":"testsbom", + "creationInfo": { "created": "2022-09-24T17:27:55.556104Z" }, "files":[ { "fileName": "file1", @@ -537,6 +547,237 @@ func Test_spdxParser(t *testing.T) { }, wantErr: false, }, + { + name: "SPDX with complex license expression", + additionalOpts: []cmp.Option{ + cmpopts.IgnoreFields(assembler.IngestPredicates{}, + "HasSBOM", "IsDependency", "IsOccurrence")}, + doc: &processor.Document{ + Blob: []byte(` +{ + "SPDXID":"SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.2", + "name":"testsbom", + "creationInfo": { + "created": "2022-09-24T17:27:55.556104Z", + "licenseListVersion": "1.2.3" + }, + "packages": [ + { + "SPDXID": "SPDXRef-35085779bdf473bb", + "name": "mypackage", + "licenseConcluded": "NOASSERTION", + "description": "Alpine base dir structure and init scripts", + "downloadLocation": "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", + "filesAnalyzed": false, + "licenseDeclared": "(BSD-3-Clause OR BSD-2-Clause) AND Apache-2.0", + "copyrightText": "Copyright (c) 2022 Authors of MyPackage", + "originator": "Person: Natanael Copa ", + "sourceInfo": "acquired package info from APK DB: /lib/apk/db/installed", + "versionInfo": "3.2.0-r22" + } + ] +} + `), + Format: processor.FormatJSON, + Type: processor.DocumentSPDX, + SourceInformation: processor.SourceInformation{ + Collector: "TestCollector", + Source: "TestSource", + }, + }, + wantPredicates: &assembler.IngestPredicates{ + CertifyLegal: []assembler.CertifyLegalIngest{ + { + Pkg: &generated.PkgInputSpec{ + Type: "guac", + Namespace: ptrfrom.String("pkg"), + Name: "mypackage", + Version: ptrfrom.String("3.2.0-r22"), + Subpath: ptrfrom.String(""), + }, + Declared: []generated.LicenseInputSpec{ + { + Name: "BSD-3-Clause", + ListVersion: ptrfrom.String("1.2.3"), + }, + { + Name: "BSD-2-Clause", + ListVersion: ptrfrom.String("1.2.3"), + }, + { + Name: "Apache-2.0", + ListVersion: ptrfrom.String("1.2.3"), + }, + }, + Discovered: []generated.LicenseInputSpec{ + { + Name: "NOASSERTION", + ListVersion: ptrfrom.String("1.2.3"), + }, + }, + CertifyLegal: &generated.CertifyLegalInputSpec{ + DeclaredLicense: "(BSD-3-Clause OR BSD-2-Clause) AND Apache-2.0", + DiscoveredLicense: "NOASSERTION", + Attribution: "Copyright (c) 2022 Authors of MyPackage", + Justification: "Found in SPDX document.", + TimeScanned: parseRfc3339("2022-09-24T17:27:55.556104Z"), + }, + }, + }, + }, + wantErr: false, + }, + { + name: "SPDX with differing licenses", + additionalOpts: []cmp.Option{ + cmpopts.IgnoreFields(assembler.IngestPredicates{}, + "HasSBOM", "IsDependency", "IsOccurrence")}, + doc: &processor.Document{ + Blob: []byte(` +{ + "SPDXID":"SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.2", + "name":"testsbom", + "creationInfo": { + "created": "2022-09-24T17:27:55.556104Z", + "licenseListVersion": "1.2.3" + }, + "packages": [ + { + "SPDXID": "SPDXRef-35085779bdf473bb", + "name": "mypackage", + "licenseConcluded": "MIT AND GPL-2.0-only", + "description": "Alpine base dir structure and init scripts", + "downloadLocation": "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", + "filesAnalyzed": false, + "licenseDeclared": "MIT", + "copyrightText": "Copyright (c) 2022 Authors of MyPackage", + "originator": "Person: Natanael Copa ", + "sourceInfo": "acquired package info from APK DB: /lib/apk/db/installed", + "versionInfo": "3.2.0-r22", + "licenseComments": "Scanned with ScanCode" + } + ] +} + `), + Format: processor.FormatJSON, + Type: processor.DocumentSPDX, + SourceInformation: processor.SourceInformation{ + Collector: "TestCollector", + Source: "TestSource", + }, + }, + wantPredicates: &assembler.IngestPredicates{ + CertifyLegal: []assembler.CertifyLegalIngest{ + { + Pkg: &generated.PkgInputSpec{ + Type: "guac", + Namespace: ptrfrom.String("pkg"), + Name: "mypackage", + Version: ptrfrom.String("3.2.0-r22"), + Subpath: ptrfrom.String(""), + }, + Declared: []generated.LicenseInputSpec{ + { + Name: "MIT", + ListVersion: ptrfrom.String("1.2.3"), + }, + }, + Discovered: []generated.LicenseInputSpec{ + { + Name: "MIT", + ListVersion: ptrfrom.String("1.2.3"), + }, + { + Name: "GPL-2.0-only", + ListVersion: ptrfrom.String("1.2.3"), + }, + }, + CertifyLegal: &generated.CertifyLegalInputSpec{ + DeclaredLicense: "MIT", + DiscoveredLicense: "MIT AND GPL-2.0-only", + Attribution: "Copyright (c) 2022 Authors of MyPackage", + Justification: "Found in SPDX document. : Scanned with ScanCode", + TimeScanned: parseRfc3339("2022-09-24T17:27:55.556104Z"), + }, + }, + }, + }, + wantErr: false, + }, + { + name: "SPDX with custom licenses", + additionalOpts: []cmp.Option{ + cmpopts.IgnoreFields(assembler.IngestPredicates{}, + "HasSBOM", "IsDependency", "IsOccurrence")}, + doc: &processor.Document{ + Blob: []byte(` +{ + "SPDXID":"SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.2", + "name":"testsbom", + "creationInfo": { + "created": "2022-09-24T17:27:55.556104Z", + "licenseListVersion": "1.2.3" + }, + "packages": [ + { + "SPDXID": "SPDXRef-35085779bdf473bb", + "name": "mypackage", + "description": "Alpine base dir structure and init scripts", + "downloadLocation": "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", + "filesAnalyzed": false, + "licenseDeclared": "LicenseRef-Custom", + "copyrightText": "Copyright (c) 2022 Authors of MyPackage", + "originator": "Person: Natanael Copa ", + "sourceInfo": "acquired package info from APK DB: /lib/apk/db/installed", + "versionInfo": "3.2.0-r22" + } + ], + "hasExtractedLicensingInfos": [ + { + "licenseId": "LicenseRef-Custom", + "extractedText": "Redistribution and use of the this code or any derivative works are permitted provided that the following conditions are met...", + "name": "Custom License" + } + ] +} +`), + Format: processor.FormatJSON, + Type: processor.DocumentSPDX, + SourceInformation: processor.SourceInformation{ + Collector: "TestCollector", + Source: "TestSource", + }, + }, + wantPredicates: &assembler.IngestPredicates{ + CertifyLegal: []assembler.CertifyLegalIngest{ + { + Pkg: &generated.PkgInputSpec{ + Type: "guac", + Namespace: ptrfrom.String("pkg"), + Name: "mypackage", + Version: ptrfrom.String("3.2.0-r22"), + Subpath: ptrfrom.String(""), + }, + Declared: []generated.LicenseInputSpec{ + { + Name: "LicenseRef-2ba8ded3", + Inline: ptrfrom.String("Redistribution and use of the this code or any derivative works are permitted provided that the following conditions are met..."), + }, + }, + CertifyLegal: &generated.CertifyLegalInputSpec{ + DeclaredLicense: "LicenseRef-2ba8ded3", + Attribution: "Copyright (c) 2022 Authors of MyPackage", + Justification: "Found in SPDX document.", + TimeScanned: parseRfc3339("2022-09-24T17:27:55.556104Z"), + }, + }, + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -558,3 +799,11 @@ func Test_spdxParser(t *testing.T) { }) } } + +func parseRfc3339(s string) time.Time { + time, err := time.Parse(time.RFC3339, s) + if err != nil { + panic(err) + } + return time +}