Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/test-upload-file.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ jobs:
tags: |
product_domain=security-agent-test

- name: Test multiple domain associations (valid)
uses: ./upload-file
with:
file_path: test-upload.json
security_agent_api_key: ${{ secrets.SECURITY_AGENT_API_KEY }}
tags: |
product_domain=security-agent-test-1,security-agent-test-2

- name: Test service name association
uses: ./upload-file
with:
Expand Down
23 changes: 2 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ The `upload-file` action uploads a file to the Security Agent. It can be used to
metadata_upload_path: ''
```

#### Service-based Upload

You can use `service=<service-name>` in `tags` to upload the metadata file to a service directory. The metadata will be uploaded to `metadata/services/<service-name>`.
#### Usage

This is compatible to run with all event types in GitHub Actions: `push`, `pull_request`, `schedule`, `workflow_dispatch`. That means that, you can use the GitHub action to store security scan reports from pull request builds, branch builds, cron builds, and manual builds.

Expand Down Expand Up @@ -82,24 +80,6 @@ jobs:
scanner=trivy
```

#### Custom Upload

If your use-case doesn't fit in the usual "service-based upload" described above, you can have more customization in which you are uploading the file to. You can use `metadata_upload_path` to specify the path to upload the metadata file.

```yaml
- name: Upload Trivy scan results to Security Agent
uses: hasura/security-agent-tools/upload-file@v1
with:
file_path: trivy-results.json
security_agent_api_key: ${{ secrets.SECURITY_AGENT_API_KEY }}
metadata_upload_path: |
lts-image-scans/${{ steps.timestamp.outputs.value }}/${{ steps.artifact-name.outputs.name }}
tags: |
scanner=trivy
image_name=${{ matrix.service.image }}
```

This will upload the metadata file to `metadata/lts-image-scans/<timestamp>/<artifact-name>/*` path whenever invoked from a GitHub workflow.

## Buildkite

Expand Down Expand Up @@ -132,6 +112,7 @@ EOF
-e BUILDKITE_BRANCH \
-e BUILDKITE_TAG \
-e BUILDKITE_PULL_REQUEST \
-e GITHUB_REPOSITORY \
ghcr.io/hasura/security-agent-tools/upload-file:v1
}

Expand Down
4 changes: 2 additions & 2 deletions upload-file/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func main() {
log.Printf("Associated image name: %s\n", imageName)
}

domain, err := sc.AssociateProductDomain(context.Background())
domain, err := sc.AssociateProductDomains(context.Background())
switch {
case err != nil:
pd, _ := catalog.ProductDomains(context.Background(), secAgentClient)
Expand All @@ -55,7 +55,7 @@ func main() {
}
log.Fatalf("Failed to associate product domain with scan: %v. Please check `product_domain` value is one of the following:\n%s", err, pds.String())
case domain != "":
log.Printf("Associated product domain: %s\n", domain)
log.Printf("Associated product domain(s): %s\n", domain)
}

serviceName, err := sc.AssociateServiceName(context.Background())
Expand Down
22 changes: 20 additions & 2 deletions upload-file/scan/associate_product_domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package scan

import (
"context"
"errors"
"strings"

"github.com/machinebox/graphql"
)

func (s *Scan) AssociateProductDomain(ctx context.Context) (string, error) {
productDomain := s.Tags["product_domain"]
func (s *Scan) associateProductDomain(ctx context.Context, productDomain string) (string, error) {
if productDomain == "" {
return "", nil
}
Expand All @@ -34,3 +35,20 @@ func (s *Scan) AssociateProductDomain(ctx context.Context) (string, error) {

return productDomain, s.client.ExecuteGQL(ctx, req, &response)
}

func (s *Scan) AssociateProductDomains(ctx context.Context) (string, error) {
productDomain := s.Tags["product_domain"]
if productDomain == "" {
return "", nil
}

var errs []error
for _, pd := range strings.Split(productDomain, ",") {
_, err := s.associateProductDomain(ctx, strings.TrimSpace(pd))
if err != nil {
errs = append(errs, err)
}
}

return productDomain, errors.Join(errs...)
}
130 changes: 115 additions & 15 deletions upload-file/scan/associate_product_domain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"github.com/hasura/security-agent-tools/upload-file/saclient"
)

func TestScan_AssociateProductDomain_Success(t *testing.T) {
expectedProductDomain := "ecommerce"
func TestScan_AssociateProductDomains_Success(t *testing.T) {
expectedProductDomain := "hasura-v2-cloud-control-plane"

// Mock GraphQL server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -38,37 +38,37 @@ func TestScan_AssociateProductDomain_Success(t *testing.T) {
client: client,
}

productDomain, err := scan.AssociateProductDomain(context.Background())
productDomain, err := scan.AssociateProductDomains(context.Background())

if err != nil {
t.Errorf("AssociateProductDomain failed: %v", err)
t.Errorf("AssociateProductDomains failed: %v", err)
}

if productDomain != expectedProductDomain {
t.Errorf("Expected product domain %s, got %s", expectedProductDomain, productDomain)
}
}

func TestScan_AssociateProductDomain_EmptyProductDomain(t *testing.T) {
func TestScan_AssociateProductDomains_EmptyProductDomain(t *testing.T) {
client := saclient.NewClient("https://example.com/graphql", "test-api-key")
scan := &Scan{
ID: "scan-id-123",
Tags: map[string]string{}, // No product_domain tag
client: client,
}

productDomain, err := scan.AssociateProductDomain(context.Background())
productDomain, err := scan.AssociateProductDomains(context.Background())

if err != nil {
t.Errorf("AssociateProductDomain failed: %v", err)
t.Errorf("AssociateProductDomains failed: %v", err)
}

if productDomain != "" {
t.Errorf("Expected empty product domain, got %s", productDomain)
}
}

func TestScan_AssociateProductDomain_GraphQLError(t *testing.T) {
func TestScan_AssociateProductDomains_GraphQLError(t *testing.T) {
// Mock GraphQL server that returns an error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
Expand All @@ -81,24 +81,124 @@ func TestScan_AssociateProductDomain_GraphQLError(t *testing.T) {
scan := &Scan{
ID: "scan-id-123",
Tags: map[string]string{
"product_domain": "ecommerce",
"product_domain": "hasura-ddn-control-plane",
},
client: client,
}

productDomain, err := scan.AssociateProductDomain(context.Background())
productDomain, err := scan.AssociateProductDomains(context.Background())

if err == nil {
t.Error("Expected error for GraphQL error response")
}

if productDomain != "ecommerce" {
if productDomain != "hasura-ddn-control-plane" {
t.Errorf("Expected product domain to be returned even on error, got %s", productDomain)
}
}

func TestScan_AssociateProductDomains_MultipleDomainsSuccess(t *testing.T) {
expectedProductDomains := "hasura-v2-cloud-control-plane, hasura-v2-cloud-data-plane, hasura-ddn-control-plane"
callCount := 0

// Mock GraphQL server that expects 3 calls
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"data": {
"insert_vulnerability_reports_by_product_domains": {
"returning": [{
"id": "assoc-id-456"
}]
}
}
}`))
}))
defer server.Close()

client := saclient.NewClient(server.URL, "test-api-key")
scan := &Scan{
ID: "scan-id-123",
Tags: map[string]string{
"product_domain": expectedProductDomains,
},
client: client,
}

productDomain, err := scan.AssociateProductDomains(context.Background())

if err != nil {
t.Errorf("AssociateProductDomains failed: %v", err)
}

if productDomain != expectedProductDomains {
t.Errorf("Expected product domain %s, got %s", expectedProductDomains, productDomain)
}

// Should have made 3 GraphQL calls (one for each domain)
if callCount != 3 {
t.Errorf("Expected 3 GraphQL calls, got %d", callCount)
}
}

func TestScan_AssociateProductDomains_MultipleDomainsPartialError(t *testing.T) {
expectedProductDomains := "hasura-v2-cloud-control-plane, hasura-v2-cloud-data-plane, hasura-ddn-control-plane"
callCount := 0

// Mock GraphQL server that fails on the second call
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

if callCount == 2 {
// Fail on second call (hasura-v2-cloud-data-plane)
w.Write([]byte(`{"errors": [{"message": "Database error"}]}`))
} else {
w.Write([]byte(`{
"data": {
"insert_vulnerability_reports_by_product_domains": {
"returning": [{
"id": "assoc-id-456"
}]
}
}
}`))
}
}))
defer server.Close()

client := saclient.NewClient(server.URL, "test-api-key")
scan := &Scan{
ID: "scan-id-123",
Tags: map[string]string{
"product_domain": expectedProductDomains,
},
client: client,
}

productDomain, err := scan.AssociateProductDomains(context.Background())

// Should return an error due to the failed second call
if err == nil {
t.Error("Expected error due to partial failure")
}

// Should still return the original product domain string
if productDomain != expectedProductDomains {
t.Errorf("Expected product domain %s, got %s", expectedProductDomains, productDomain)
}

// Should have made 3 GraphQL calls (one for each domain)
if callCount != 3 {
t.Errorf("Expected 3 GraphQL calls, got %d", callCount)
}
}

func TestProductDomains_Success(t *testing.T) {
expectedDomains := []string{"ecommerce", "analytics", "security"}
expectedDomains := []string{"hasura-v2-cloud-control-plane", "hasura-v2-cloud-data-plane", "hasura-ddn-control-plane"}

// Mock GraphQL server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -107,9 +207,9 @@ func TestProductDomains_Success(t *testing.T) {
w.Write([]byte(`{
"data": {
"vulnerability_reports_product_domains": [
{"code": "ecommerce"},
{"code": "analytics"},
{"code": "security"}
{"code": "hasura-v2-cloud-control-plane"},
{"code": "hasura-v2-cloud-data-plane"},
{"code": "hasura-ddn-control-plane"}
]
}
}`))
Expand Down
Loading