diff --git a/.github/workflows/ci-integration.yaml b/.github/workflows/ci-integration.yaml index b7fc9eda3d..37b5fe8efb 100644 --- a/.github/workflows/ci-integration.yaml +++ b/.github/workflows/ci-integration.yaml @@ -32,6 +32,7 @@ permissions: jobs: test-integration: + if: github.repository != 'verkada/guac' runs-on: ubuntu-latest name: CI for integration tests steps: @@ -65,6 +66,7 @@ jobs: run: make integration-test end-to-end: + if: github.repository != 'verkada/guac' name: E2E runs-on: ubuntu-latest services: @@ -119,6 +121,7 @@ jobs: GUAC_DIR: /home/runner/work/guac/guac tilt-ci: + if: github.repository != 'verkada/guac' name: Run 'tilt ci' runs-on: labels: ubuntu-latest diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 33b6c14d4d..3919913729 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -28,6 +28,7 @@ permissions: jobs: goreleaser: + if: github.repository != 'verkada/guac' runs-on: ubuntu-latest outputs: hashes: ${{ steps.hash.outputs.hashes }} @@ -105,7 +106,7 @@ jobs: build-atlas: runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: packages: write # To publish container images to GHCR id-token: write # To use our OIDC token @@ -140,7 +141,7 @@ jobs: name: generate sbom for container runs-on: ubuntu-latest needs: [goreleaser] - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: id-token: write # needed for signing the images with GitHub OIDC Token packages: write # needed to upload signatures @@ -177,7 +178,7 @@ jobs: provenance-bins: name: generate provenance for binaries needs: [goreleaser] - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: id-token: write # To sign the provenance contents: write # To upload assets to release @@ -190,7 +191,7 @@ jobs: provenance-container: name: generate provenance for container needs: [goreleaser] - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: id-token: write # To sign the provenance contents: write # To upload assets to release @@ -208,7 +209,7 @@ jobs: runs-on: ubuntu-latest name: generate compose tarball needs: [goreleaser] - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: contents: write # To upload assets to release. packages: write # To publish container images to GHCR diff --git a/.github/workflows/security-pr-checks.yml b/.github/workflows/security-pr-checks.yml new file mode 100644 index 0000000000..64be1aa0e3 --- /dev/null +++ b/.github/workflows/security-pr-checks.yml @@ -0,0 +1,13 @@ +name: security-pr-checks +on: + # Allow for manual run of security workflows + workflow_dispatch: + # Scan changed files in PRs (diff-aware scanning): + pull_request: {} +jobs: + running-pr-security-checks: + uses: verkada/securitybots/.github/workflows/pr-checks.yml@main + secrets: inherit + running-pr-semgrep-check: + uses: verkada/securitybots/.github/workflows/semgrep-pr-checks.yml@main + secrets: inherit diff --git a/.github/workflows/verkada-release.yaml b/.github/workflows/verkada-release.yaml new file mode 100644 index 0000000000..545f21506f --- /dev/null +++ b/.github/workflows/verkada-release.yaml @@ -0,0 +1,51 @@ +# +# Copyright 2022 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. + +name: verkada-release + +on: + workflow_dispatch: # Manual trigger + push: + tags: + - "v*-verkada-*" + +permissions: + contents: write # To create releases and upload assets + +jobs: + build-binaries: + runs-on: ubuntu-latest + permissions: + contents: write # To upload release assets + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: '1.24' + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + distribution: goreleaser + version: latest + args: release --clean -f .goreleaser-verkada.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/verkada-update-release.yaml b/.github/workflows/verkada-update-release.yaml new file mode 100644 index 0000000000..d66b05dbe7 --- /dev/null +++ b/.github/workflows/verkada-update-release.yaml @@ -0,0 +1,233 @@ +name: Update Real Remote to Fork + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + test_branch: + description: 'Optional: Branch to test with (leave empty to use main)' + required: false + type: string + schedule: + # run every night @ 01:00 am UTC + - cron: '1 0 * * *' + +jobs: + fetch-updates: + runs-on: ubuntu-latest + environment: upstream-link + timeout-minutes: 5 + permissions: + id-token: write + contents: write + outputs: + start_time: ${{ steps.set_start_time.outputs.time }} + + steps: + - name: Set branch name + id: set-branch + env: + TEST_BRANCH: ${{ github.event.inputs.test_branch }} + EVENT_NAME: ${{ github.event_name }} + GITHUB_REF_ENV: ${{ github.ref }} + run: | + if [ -n "${TEST_BRANCH}" ]; then + echo "branch_name=${TEST_BRANCH}" >> ${GITHUB_OUTPUT} + elif [ "${EVENT_NAME}" == "push" ]; then + echo "branch_name=${GITHUB_REF_ENV#refs/heads/}" >> ${GITHUB_OUTPUT} + else + echo "branch_name=main" >> ${GITHUB_OUTPUT} + fi + + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v5.1.0 + with: + role-duration-seconds: 900 + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_NUMBER }}:role/vlnx-mirror-ci-github-action-role + role-session-name: GithubActionTurnstile + aws-region: us-west-2 + + - name: Checkout branch + uses: actions/checkout@v4 + with: + ref: ${{ steps.set-branch.outputs.branch_name }} + submodules: recursive + fetch-depth: 0 + + - name: Set job start time + id: set_start_time + run: | + echo "time=$(date -u +%s)" >> $GITHUB_OUTPUT + echo "Set start time: $(date -u +%s)" + + - name: Configure git + run: | + git config --global user.email "device-platform-github-bot@noreply.verkada.com" + git config --global user.name "Device Platform GitHub Bot" + + - name: Update + env: + BRANCH_NAME: ${{ steps.set-branch.outputs.branch_name }} + UPSTREAM_URL: ${{ vars.UPSTREAM_URL }} + UPSTREAM_BRANCH: main + run: | + git fetch -p origin + git checkout "${BRANCH_NAME}" + + + # Note: upstream needs to point to the origin repo + git remote add upstream "${UPSTREAM_URL}" + git fetch -p upstream + + if git diff --quiet origin/${BRANCH_NAME}..upstream/${BRANCH_NAME}; then + echo "No changes detected" + exit 0 + fi + + git rebase upstream/${BRANCH_NAME} + + - name: Push Updates + id: push-branch + env: + BRANCH_NAME: ${{ steps.set-branch.outputs.branch_name }} + run: | + git push + + - name: Post job failure status to slack + if: ${{ ! success() }} + uses: slackapi/slack-github-action@v1.24.0 + with: + # device-platform-alerts + channel-id: 'C083RN0EPEE' + # For posting a simple plain text message + slack-message: | + ${{ github.repository }}: ${{ github.workflow }}: *${{ job.status }}* + ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + <@U025XPBNJR5> <@U024C1144BE> <@U083E9XKYDN> + # nick, sam, natsumi + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + - name: Post to turnstile + if: ${{ !cancelled() }} + env: + JOB_SUCCESS: ${{ job.status == 'success' }} + JOB_RESULT: ${{ job.status }} + WORKFLOW_NAME: ${{ github.workflow }} + RUN_ID: ${{ github.run_id }} + RUN_NUMBER: ${{ github.run_number }} + START_TIME: ${{ steps.set_start_time.outputs.time }} + GITHUB_EVENT_SCHEDULE: ${{ github.event.schedule }} + GITHUB_REPO_NAME: ${{ github.repository }} + GITHUB_SHA: ${{ github.sha }} + S3_BUCKET: verkada-device-platform-vlnx-ci + run: | + # Create temp directory + TEMP_DIR="$(mktemp -d)" + + # Capture end time for runtime calculation + END_TIME="$(date -u +%s)" + + # Calculate runtime in seconds if we have start time + if [[ "${START_TIME}" =~ ^[0-9]+$ ]]; then + # START_TIME is already a Unix timestamp + RUNTIME="$((${END_TIME} - ${START_TIME}))" + elif [[ "${START_TIME}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then + # START_TIME is an ISO format string, convert to timestamp + START_EPOCH="$(date -u -d "${START_TIME}" +%s)" + RUNTIME="$((${END_TIME} - ${START_EPOCH}))" + else + RUNTIME=0 + fi + + # Determine success status from job result + SUCCESS=true + if [ "${JOB_SUCCESS}" != "true" ]; then + SUCCESS=false + fi + + # Use the provided cron schedule input directly + CRON_SCHEDULE="${GITHUB_EVENT_SCHEDULE:-}" + echo "Cron schedule: ${CRON_SCHEDULE:-'not scheduled'}" + + # Validate required fields before creating JSON + echo "Validating required fields..." + + # Validate start_time is present and valid + if [ -z "${START_TIME}" ]; then + echo "❌ Error: start_time is required but missing" + exit 1 + fi + + # Validate start_time is a valid timestamp + if ! [[ "${START_TIME}" =~ ^[0-9]+$ ]] && ! [[ "${START_TIME}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then + echo "❌ Error: start_time must be a Unix timestamp or ISO format" + exit 1 + fi + + # Validate cron_schedule is present and not empty (only for scheduled runs) + if [ -z "${CRON_SCHEDULE}" ]; then + echo "⚠️ Warning: cron_schedule is empty (workflow not triggered by schedule)" + CRON_SCHEDULE="" + fi + + echo "✅ All required fields validated" + + # Create workflow run URL + WORKFLOW_URL="https://github.com/${GITHUB_REPO_NAME}/actions/runs/${RUN_ID}" + + # Create JSON result file using jq to handle special characters safely + jq -n \ + --arg workflow "${WORKFLOW_NAME}" \ + --arg run_id "${RUN_ID}" \ + --arg run_number "${RUN_NUMBER}" \ + --arg repository "${GITHUB_REPO_NAME}" \ + --arg commit "${GITHUB_SHA}" \ + --arg started_at "${START_TIME}" \ + --arg completed_at "${END_TIME}" \ + --arg runtime_seconds "${RUNTIME}" \ + --arg success "${SUCCESS}" \ + --arg cron_schedule "${CRON_SCHEDULE}" \ + --arg workflow_url "${WORKFLOW_URL}" \ + '{ + workflow: $workflow, + run_id: $run_id, + run_number: $run_number, + repository: $repository, + commit: $commit, + started_at: $started_at, + completed_at: $completed_at, + runtime_seconds: $runtime_seconds, + success: $success, + cron_schedule: $cron_schedule, + workflow_url: $workflow_url + }' > "${TEMP_DIR}/result.json" + + # Upload to S3 + echo "Uploading result to S3..." + + # Strip "verkada/" prefix from repository name for cleaner S3 paths + if [[ "${GITHUB_REPO_NAME}" == "verkada/"* ]]; then + CLEAN_REPO_NAME="${GITHUB_REPO_NAME#verkada/}" + else + CLEAN_REPO_NAME="${GITHUB_REPO_NAME}" + fi + + # Sanitize workflow name to replace forward slashes with hyphens for clean S3 paths + CLEAN_WORKFLOW_NAME="${WORKFLOW_NAME//\//-}" + + S3_PREFIX="s3://${S3_BUCKET}/workflow_results/${CLEAN_REPO_NAME}/${CLEAN_WORKFLOW_NAME}" + S3_PATH="${S3_PREFIX}/result-${START_TIME}.json" + S3_PATH_LATEST="${S3_PREFIX}/result-latest.json" + + # Upload the JSON file + aws s3 cp "${TEMP_DIR}/result.json" "${S3_PATH}" + aws s3 cp "${TEMP_DIR}/result.json" "${S3_PATH_LATEST}" + + echo "Result uploaded to S3: ${S3_PATH}" + + # Clean up temp directory + rm -rf "${TEMP_DIR}" + diff --git a/.goreleaser-verkada.yaml b/.goreleaser-verkada.yaml new file mode 100644 index 0000000000..fa897e4f18 --- /dev/null +++ b/.goreleaser-verkada.yaml @@ -0,0 +1,106 @@ +--- +project_name: guac +version: 2 + +env: + - CGO_ENABLED=0 + - PKG=github.com/guacsec/guac/pkg/version + +before: + hooks: + - go mod tidy + - go generate ./... + +builds: + - main: ./cmd/guaccollect + id: guaccollect + binary: guaccollect-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['linux'] + goarch: + - amd64 + - arm64 + + - main: ./cmd/guaccsub + id: guaccsub + binary: guaccsub-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['linux'] + goarch: + - amd64 + - arm64 + + - main: ./cmd/guacgql + id: guacgql + binary: guacgql-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['linux'] + goarch: + - amd64 + - arm64 + + - main: ./cmd/guacingest + id: guacingest + binary: guacingest-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['linux'] + goarch: + - amd64 + - arm64 + + - main: ./cmd/guacone + id: guacone + binary: guacone-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['darwin', 'linux'] + goarch: + - amd64 + - arm64 + +archives: + - formats: [binary] + name_template: "{{ .Binary }}" + allow_different_binary_count: true + +checksum: + name_template: "guac_checksums.txt" + +snapshot: + version_template: SNAPSHOT-{{ .ShortCommit }} + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +release: + prerelease: auto + draft: false + replace_existing_draft: true + name_template: "Verkada GUAC {{.Tag}}" + header: | + ## Verkada Custom GUAC Release + + This release contains custom modifications for Verkada's use case: + - VEX status mappings for Vigiles SBOM ingestion + - Enhanced empty analysis state handling + - Improved RFC3339 timestamp parsing + + Base version: GUAC v1.0.1 diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000000..d44e60d6f6 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,91 @@ +# BUILD.bazel for GUAC binaries +# This file builds GUAC binaries from the forked repo for use in vguac +# +# NOTE: This uses genrule to build binaries using `go build` directly. +# This requires go.mod and all dependencies to be available in the guac repo. +# For a more hermetic build, consider using gazelle to generate BUILD files. +# +# The binaries are built for linux/arm64 to match vguac's architecture requirements. + +package(default_visibility = ["//visibility:public"]) + +# GUAC GraphQL server binary +genrule( + name = "guacgql", + srcs = glob([ + "cmd/guacgql/**/*.go", + "pkg/**/*.go", + "internal/**/*.go", + "go.mod", + "go.sum", + ]), + outs = ["guacgql"], + cmd = """ + export GOOS=linux + export GOARCH=arm64 + cd $$(dirname $(location go.mod)) + go build -o $@ ./cmd/guacgql + """, + visibility = ["//visibility:public"], +) + +# GUAC Ingestor binary +genrule( + name = "guacingest", + srcs = glob([ + "cmd/guacingest/**/*.go", + "pkg/**/*.go", + "internal/**/*.go", + "go.mod", + "go.sum", + ]), + outs = ["guacingest"], + cmd = """ + export GOOS=linux + export GOARCH=arm64 + cd $$(dirname $(location go.mod)) + go build -o $@ ./cmd/guacingest + """, + visibility = ["//visibility:public"], +) + +# GUAC Collector binary +genrule( + name = "guaccollect", + srcs = glob([ + "cmd/guaccollect/**/*.go", + "pkg/**/*.go", + "internal/**/*.go", + "go.mod", + "go.sum", + ]), + outs = ["guaccollect"], + cmd = """ + export GOOS=linux + export GOARCH=arm64 + cd $$(dirname $(location go.mod)) + go build -o $@ ./cmd/guaccollect + """, + visibility = ["//visibility:public"], +) + +# GUAC CollectSub binary +genrule( + name = "guaccsub", + srcs = glob([ + "cmd/guaccsub/**/*.go", + "pkg/**/*.go", + "internal/**/*.go", + "go.mod", + "go.sum", + ]), + outs = ["guaccsub"], + cmd = """ + export GOOS=linux + export GOARCH=arm64 + cd $$(dirname $(location go.mod)) + go build -o $@ ./cmd/guaccsub + """, + visibility = ["//visibility:public"], +) + diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json b/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json index a530ad2ac3..2a35b67e5e 100644 --- a/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json +++ b/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json @@ -45,38 +45,6 @@ ] } ] - }, - { - "id": "CVE-2024-0004", - "source": { - "name": "NVD", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0004" - }, - "ratings": [ - { - "source": { - "name": "NVD", - "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator" - }, - "score": 6.0, - "severity": "medium", - "method": "CVSSv31" - } - ], - "analysis": { - "state": "false_positive" - }, - "affects": [ - { - "ref": "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80/1#test-component-2-no-detail", - "versions": [ - { - "version": "1.0.0", - "status": "unaffected" - } - ] - } - ] } ] } diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json b/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json index 24812ce25b..d5420774b7 100644 --- a/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json +++ b/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json @@ -45,38 +45,6 @@ ] } ] - }, - { - "id": "CVE-2024-0003", - "source": { - "name": "NVD", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0003" - }, - "ratings": [ - { - "source": { - "name": "NVD", - "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator" - }, - "score": 7.5, - "severity": "high", - "method": "CVSSv31" - } - ], - "analysis": { - "state": "resolved_with_pedigree" - }, - "affects": [ - { - "ref": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79/1#test-component-no-detail", - "versions": [ - { - "version": "1.0.0", - "status": "affected" - } - ] - } - ] } ] } diff --git a/internal/testing/testdata/testdata.go b/internal/testing/testdata/testdata.go index 6172562909..c2acd7ed2a 100644 --- a/internal/testing/testdata/testdata.go +++ b/internal/testing/testdata/testdata.go @@ -326,22 +326,7 @@ var ( StatusNotes: "Vulnerability was falsely identified or associated with this component", KnownSince: time.Unix(0, 0).UTC(), } - // VexData for resolved_with_pedigree status without detail (maps to VexStatusFixed) - VexDataResolvedWithPedigreeNoDetail = &generated.VexStatementInputSpec{ - Status: generated.VexStatusFixed, - VexJustification: generated.VexJustificationNotProvided, - Statement: "", - StatusNotes: "CDX state: resolved_with_pedigree", - KnownSince: time.Unix(0, 0).UTC(), - } - // VexData for false_positive status without detail (maps to VexStatusNotAffected) - VexDataFalsePositiveNoDetail = &generated.VexStatementInputSpec{ - Status: generated.VexStatusNotAffected, - VexJustification: generated.VexJustificationNotProvided, - Statement: "", - StatusNotes: "CDX state: false_positive", - KnownSince: time.Unix(0, 0).UTC(), - } + // Vulnerability specs for new test cases VulnSpecResolvedWithPedigree = &generated.VulnerabilityInputSpec{ Type: "cve", VulnerabilityID: "cve-2024-0001", @@ -350,15 +335,6 @@ var ( Type: "cve", VulnerabilityID: "cve-2024-0002", } - // Vulnerability specs for no-detail test cases - VulnSpecResolvedWithPedigreeNoDetail = &generated.VulnerabilityInputSpec{ - Type: "cve", - VulnerabilityID: "cve-2024-0003", - } - VulnSpecFalsePositiveNoDetail = &generated.VulnerabilityInputSpec{ - Type: "cve", - VulnerabilityID: "cve-2024-0004", - } // VulnMetadata for resolved_with_pedigree test CycloneDXResolvedWithPedigreeVulnMetadata = []assembler.VulnMetadataIngest{ { @@ -369,14 +345,6 @@ var ( Timestamp: time.Unix(0, 0).UTC(), }, }, - { - Vulnerability: VulnSpecResolvedWithPedigreeNoDetail, - VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ - ScoreType: generated.VulnerabilityScoreTypeCvssv31, - ScoreValue: 7.5, - Timestamp: time.Unix(0, 0).UTC(), - }, - }, } // VulnMetadata for false_positive test CycloneDXFalsePositiveVulnMetadata = []assembler.VulnMetadataIngest{ @@ -388,14 +356,6 @@ var ( Timestamp: time.Unix(0, 0).UTC(), }, }, - { - Vulnerability: VulnSpecFalsePositiveNoDetail, - VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ - ScoreType: generated.VulnerabilityScoreTypeCvssv31, - ScoreValue: 6.0, - Timestamp: time.Unix(0, 0).UTC(), - }, - }, } topLevelPkg, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/ABC") @@ -427,7 +387,7 @@ var ( HasSBOM: &model.HasSBOMInputSpec{ Uri: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", Algorithm: "sha256", - Digest: "32981b0c4f87df9243c0e9b8a9600f2e19aae0c0cb76122edfe4a54ef59b9d48", + Digest: "a9e5e5fcc0939b4e9ddf74a5863ff577bef9bbf8086d99a4dafb8154c451b56f", KnownSince: parseRfc3339("2024-01-15T10:30:00Z"), }, }, @@ -440,25 +400,22 @@ var ( HasSBOM: &model.HasSBOMInputSpec{ Uri: "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80", Algorithm: "sha256", - Digest: "0731373583749ae046d0992e9b417d4b2960f75d7a979c72fd0b7a258566d520", + Digest: "738690dd4acaf82b417072354ee631a20a50453278053b558770c6f65906f11d", KnownSince: parseRfc3339("2024-01-15T10:30:00Z"), }, }, } // Predicates for resolved_with_pedigree test + // The affects ref is "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79/1#test-component" + // The parser splits on "#" and uses "test-component" as pkdIdentifier + // Then creates PURL as pkg:guac/pkg/test-component@1.0.0 using guacCDXPkgPurl resolvedWithPedigreePkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component@1.0.0") - resolvedWithPedigreeNoDetailPkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-no-detail@1.0.0") CycloneDXResolvedWithPedigreeVexIngest = []assembler.VexIngest{ { Pkg: resolvedWithPedigreePkg, Vulnerability: VulnSpecResolvedWithPedigree, VexData: VexDataResolvedWithPedigree, }, - { - Pkg: resolvedWithPedigreeNoDetailPkg, - Vulnerability: VulnSpecResolvedWithPedigreeNoDetail, - VexData: VexDataResolvedWithPedigreeNoDetail, - }, } CycloneDXResolvedWithPedigreePredicates = assembler.IngestPredicates{ HasSBOM: HasSBOMVexResolvedWithPedigree, @@ -467,24 +424,22 @@ var ( // Note: No CertifyVuln because status is Fixed (not Affected/UnderInvestigation) } // Predicates for false_positive test + // The affects ref is "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80/1#test-component-2" + // The parser splits on "#" and uses "test-component-2" as pkdIdentifier + // Then creates PURL as pkg:guac/pkg/test-component-2@1.0.0 using guacCDXPkgPurl falsePositivePkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-2@1.0.0") - falsePositiveNoDetailPkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-2-no-detail@1.0.0") CycloneDXFalsePositiveVexIngest = []assembler.VexIngest{ { Pkg: falsePositivePkg, Vulnerability: VulnSpecFalsePositive, VexData: VexDataFalsePositive, }, - { - Pkg: falsePositiveNoDetailPkg, - Vulnerability: VulnSpecFalsePositiveNoDetail, - VexData: VexDataFalsePositiveNoDetail, - }, } CycloneDXFalsePositivePredicates = assembler.IngestPredicates{ HasSBOM: HasSBOMVexFalsePositive, VulnMetadata: CycloneDXFalsePositiveVulnMetadata, Vex: CycloneDXFalsePositiveVexIngest, + // Note: No CertifyVuln because status is NotAffected (not Affected/UnderInvestigation) } // DSSE/SLSA Testdata @@ -2400,36 +2355,29 @@ var ( }` VertxWebAttestation = `{ - "type": "https://in-toto.io/Statement/v1", - "subject": [ - { - "uri": "pkg:maven/io.vertx/vertx-web@4.3.7?type=jar" - } - ], - "predicate_type": "https://in-toto.io/attestation/vulns/v0.1", - "predicate": { - "scanner": { - "uri": "osv.dev", - "version": "0.0.14", - "db": {}, - "result": [ - { - "id": "GHSA-45p5-v273-3qqr" - }, + "type": "https://in-toto.io/Statement/v1", + "subject": [ { - "id": "GHSA-53jx-vvf9-4x38" + "uri": "pkg:maven/io.vertx/vertx-web@4.3.7?type=jar" + } + ], + "predicate_type": "https://in-toto.io/attestation/vulns/v0.1", + "predicate": { + "scanner": { + "uri": "osv.dev", + "version": "0.0.14", + "result": [ + { + "id": "GHSA-53jx-vvf9-4x38" + } + ] }, - { - "id": "GHSA-h5fg-jpgr-rv9c" + "metadata": { + "scanStartedOn":"2023-02-15T11:10:08.986506-08:00", + "scanFinishedOn":"2023-02-15T11:10:08.986506-08:00" } - ] - }, - "metadata": { - "scanStartedOn": "2025-12-01T15:19:40.545851224Z", - "scanFinishedOn": "2025-12-01T15:19:40.545851224Z" - } - } - }` + } + }` VertxWebCommonPackage = root_package.PackageNode{ Purl: "pkg:maven/io.vertx/vertx-web-common@4.3.7?type=jar", diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go index 44b8d806a5..c8a67a8006 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go @@ -383,6 +383,9 @@ func getArtifactInput(subject string) (*model.ArtifactInputSpec, error) { func (c *cyclonedxParser) GetPredicates(ctx context.Context) *assembler.IngestPredicates { logger := logging.FromContext(ctx) + defer func() { + c.doc = nil + }() preds := &assembler.IngestPredicates{} var topLevelArts []*model.ArtifactInputSpec var topLevelPkgs []*model.PkgInputSpec @@ -558,10 +561,17 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { var vd model.VexStatementInputSpec publishedTime := zeroTime if vulnerability.Analysis != nil { - if vexStatus, ok := vexStatusMap[vulnerability.Analysis.State]; ok { - vd.Status = vexStatus + // Only set status if state is not empty + if vulnerability.Analysis.State != "" { + if vexStatus, ok := vexStatusMap[vulnerability.Analysis.State]; ok { + vd.Status = vexStatus + } else { + return fmt.Errorf("unknown vulnerability status %s", vulnerability.Analysis.State) + } } else { - return fmt.Errorf("unknown vulnerability status %s", vulnerability.Analysis.State) + // If state is empty, use UNDER_INVESTIGATION to indicate analysis pending + // The affects array may indicate the component is affected, but VEX analysis hasn't been performed + vd.Status = model.VexStatusUnderInvestigation } if vexJustification, ok := justificationsMap[vulnerability.Analysis.Justification]; ok { @@ -571,9 +581,22 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { } if vulnerability.Published != "" { + // Try RFC3339 first, then try various formats publishedTime, err = time.Parse(time.RFC3339, vulnerability.Published) if err != nil { - return fmt.Errorf("failed to pase time: %s, with error: %w", vulnerability.Published, err) + // Try RFC3339 without seconds (e.g., "2025-01-29T00:00Z") + publishedTime, err = time.Parse("2006-01-02T15:04Z", vulnerability.Published) + if err != nil { + // Try parsing without timezone (e.g., "2023-08-22T19:16:31.080") + publishedTime, err = time.Parse("2006-01-02T15:04:05.000", vulnerability.Published) + if err != nil { + // Try parsing without milliseconds + publishedTime, err = time.Parse("2006-01-02T15:04:05", vulnerability.Published) + if err != nil { + return fmt.Errorf("failed to parse time: %s, with error: %w", vulnerability.Published, err) + } + } + } } } vd.KnownSince = publishedTime diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go index 5f607fe0b0..89b624f4e3 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go @@ -241,6 +241,26 @@ func Test_cyclonedxParser(t *testing.T) { } } +func TestCycloneDXParserClearsDocAfterParse(t *testing.T) { + ctx := logging.WithLogger(context.Background()) + p := &cyclonedxParser{} + + doc := &processor.Document{ + Blob: testdata.CycloneDXDistrolessExample, + Format: processor.FormatJSON, + Type: processor.DocumentCycloneDX, + } + + if err := p.Parse(ctx, doc); err != nil { + t.Fatalf("Parse returned error: %v", err) + } + // Build predicates to mirror normal flow, then ensure the document is released. + _ = p.GetPredicates(ctx) + if p.doc != nil { + t.Fatalf("expected doc to be cleared after GetPredicates, got non-nil") + } +} + func Test_cyclonedxParser_addRootPackage(t *testing.T) { tests := []struct { name string